From 4aaf756b4700e8ff546fa1253409d3f016403755 Mon Sep 17 00:00:00 2001 From: John Smith Date: Wed, 26 Aug 2026 01:07:01 -0400 Subject: [PATCH 001/243] chore: add lefthook git hooks and polyglot static analysis toolchain - lefthook (pnpm devDep, auto-installed via prepare script): pre-commit runs prettier + eslint --fix on staged files, ast-grep structural scan, gitleaks secret scan; pre-push runs server typecheck, per-crate clippy, golangci-lint (--new-from-rev rollout mode), knip report-only - knip.json workspace config; baseline recorded in CLAUDE.md - sgconfig.yml + starter rules (no-console-log TS/Vue, no-fmt-println Go) - backend/.golangci.yml v2 with gosec; golangci-lint job added to CI - security.yml: gitleaks history scan, cargo-audit matrix over all 7 Rust crates, weekly scheduled semgrep SARIF upload (semgrep-scheduled.yml) - root .prettierrc.json shared across the workspace - server-ci.yml: non-blocking knip report artifact job Co-Authored-By: Claude Opus 5 --- .github/workflows/security.yml | 65 ++ .github/workflows/semgrep-scheduled.yml | 27 + .github/workflows/server-ci.yml | 29 + .prettierrc.json | 5 + CLAUDE.md | 63 ++ backend/.golangci.yml | 27 + knip.json | 31 + lefthook.yml | 74 ++ package.json | 14 +- pnpm-lock.yaml | 914 +++++++++++++++++++++++- pnpm-workspace.yaml | 2 + rules/no-console-log.yml | 12 + rules/no-fmt-println.yml | 6 + scripts/clippy-changed.sh | 46 ++ sgconfig.yml | 5 + 15 files changed, 1284 insertions(+), 36 deletions(-) create mode 100644 .github/workflows/security.yml create mode 100644 .github/workflows/semgrep-scheduled.yml create mode 100644 .prettierrc.json create mode 100644 CLAUDE.md create mode 100644 backend/.golangci.yml create mode 100644 knip.json create mode 100644 lefthook.yml create mode 100644 rules/no-console-log.yml create mode 100644 rules/no-fmt-println.yml create mode 100755 scripts/clippy-changed.sh create mode 100644 sgconfig.yml diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml new file mode 100644 index 000000000..cc82d0a72 --- /dev/null +++ b/.github/workflows/security.yml @@ -0,0 +1,65 @@ +name: Security + +on: + push: + branches: [develop] + pull_request: + branches: [develop] + schedule: + # Weekly Monday 06:06 UTC — catches new advisories between PRs. + - cron: "6 6 * * 1" + workflow_dispatch: + +permissions: + contents: read + +jobs: + gitleaks: + name: Secret scan + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 # full history for leak detection + - uses: gitleaks/gitleaks-action@v2 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + cargo-audit: + name: Rust advisory audit + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + crate: + - cli + - torrential + - desktop/src-tauri + - libraries/droplet + - libraries/droplet_types + - libraries/libarchive + - libraries/native_model + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@nightly # matches droplet-ci + - uses: taiki-e/install-action@cargo-audit + - name: Audit ${{ matrix.crate }} + run: cargo audit --file Cargo.lock + working-directory: ${{ matrix.crate }} + + golangci: + name: Go lint (backend) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 # needed for --new-from-rev against origin/develop + - uses: actions/setup-go@v5 + with: + go-version-file: backend/go.work + - uses: golangci/golangci-lint-action@v8 + with: + working-directory: backend + # Rollout mode: only report issues on changed lines. Remove once the + # existing debt (2 issues in core/database.go) is cleaned up. + args: --new-from-rev=origin/develop ./core/... diff --git a/.github/workflows/semgrep-scheduled.yml b/.github/workflows/semgrep-scheduled.yml new file mode 100644 index 000000000..85f56d78e --- /dev/null +++ b/.github/workflows/semgrep-scheduled.yml @@ -0,0 +1,27 @@ +name: Semgrep (scheduled) + +# Deep security scan on a weekly schedule — deliberately not wired into git +# hooks or push/PR CI, where its runtime would be too disruptive. +on: + schedule: + - cron: "30 5 * * 1" # weekly Monday 05:30 UTC + workflow_dispatch: + +permissions: + contents: read + security-events: write + +jobs: + semgrep: + runs-on: ubuntu-latest + container: + image: semgrep/semgrep + steps: + - uses: actions/checkout@v4 + - name: Scan + run: semgrep scan --config p/default --sarif --output semgrep.sarif + - name: Upload SARIF + if: always() + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: semgrep.sarif diff --git a/.github/workflows/server-ci.yml b/.github/workflows/server-ci.yml index 99511123c..8adb66060 100644 --- a/.github/workflows/server-ci.yml +++ b/.github/workflows/server-ci.yml @@ -69,3 +69,32 @@ jobs: - name: Lint working-directory: server run: pnpm run lint + + knip: + name: Dependency & export report (non-blocking) + runs-on: ubuntu-latest + continue-on-error: true # report-only rollout phase; remove once triaged + steps: + - name: Check out the repo + uses: actions/checkout@v4 + + - name: Install pnpm + uses: pnpm/action-setup@v4 + + - name: Setup Node.js environment + uses: actions/setup-node@v4 + with: + node-version: lts/* + cache: "pnpm" + + - name: Install dependencies + run: pnpm install + + - name: Knip report + run: pnpm exec knip --reporter json > knip-report.json || true + + - name: Upload knip report + uses: actions/upload-artifact@v4 + with: + name: knip-report + path: knip-report.json diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 000000000..30581eb7d --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,5 @@ +{ + "jsonRecursiveSort": true, + "jsonSortOrder": "{\"/.*/\": \"lexical\"}", + "plugins": ["prettier-plugin-sort-json"] +} diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..12d76b3bc --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,63 @@ +# Drop + +Self-hosted game distribution platform. Polyglot monorepo: + +- `server/` — Nuxt 3 + Vue 3 app (TypeScript, Prisma, Tailwind, buf/protobuf) +- `backend/` — Go backend (go.work, module under `core/`) +- Rust crates: `cli/` (downpour), `torrential/`, `desktop/src-tauri/`, `libraries/{droplet,droplet_types,libarchive,native_model}` +- pnpm workspace: root + `server/`, `libraries/base/`, `sites/*`, `desktop/` + +## Quality gates — what runs when + +| Layer | When | What | +| ------------------- | ---------- | ------------------------------------------------------------------------------------- | +| Claude Code hooks | every edit | format-on-edit (advisory) | +| lefthook pre-commit | commit | prettier + eslint --fix on staged files, ast-grep scan, gitleaks | +| lefthook pre-push | push | server typecheck, clippy (changed crates), golangci-lint, knip report | +| GitHub Actions | PR/push | typecheck/lint/clippy + gitleaks history scan + cargo-audit ×7 crates + golangci-lint | +| GitHub Actions | weekly | semgrep deep scan → Code Scanning | + +Hooks are early feedback; **CI is the authority**. Never disable a hook to make a +failure go away — read the output and fix it. + +### Escape hatches + +```sh +git commit --no-verify # skip pre-commit once +git push --no-verify # skip pre-push once +LEFTHOOK=0 git commit # same via env var +``` + +## Commands + +```sh +pnpm exec lefthook run pre-commit --all-files # dry-run all pre-commit checks +pnpm exec lefthook run pre-push --all-files # dry-run all pre-push checks +pnpm exec ast-grep scan [paths] # structural lint (sgconfig.yml) +pnpm exec ast-grep scan --json # machine-readable output +pnpm exec knip --reporter compact # unused deps/exports/files report +cd backend && golangci-lint run ./core/... # Go lint (.golangci.yml in backend/) +gitleaks protect --staged # secret scan staged changes +``` + +Native binaries NOT installable via pnpm: + +- `gitleaks` — `brew install gitleaks` +- `cargo-audit` — `cargo install cargo-audit` +- `golangci-lint` — `go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest` + (must be built with the same Go version as `backend/go.work` or typechecking fails) + +## Rollout status (flip these when clean) + +- **knip**: report-only (`continue-on-error` in CI, `|| true` in hook). Baseline: + 117 unused files / 31 unused exports / 4 unused deps. Remove the escape hatches + to enforce. +- **golangci-lint**: `--new-from-rev=origin/develop` (new issues only). Baseline: + 2 legacy issues in `core/database.go`. Remove flag when count is 0. +- **ast-grep** rules: `severity: warning`. Promote per-rule to `error` after cleanup. + +## Conventions + +- Rust: nightly toolchain (matches CI); clippy with `-D warnings`. +- Formatting: Prettier config at repo root (shared by all JS/TS/Vue packages). +- Go: format with gofmt/goimports; keep modules inside `backend/` consistent with `go.work`. diff --git a/backend/.golangci.yml b/backend/.golangci.yml new file mode 100644 index 000000000..5992b48e7 --- /dev/null +++ b/backend/.golangci.yml @@ -0,0 +1,27 @@ +# golangci-lint v2 configuration. +# Run: cd backend && golangci-lint run +# CI: golangci-lint-action@v8 with --new-from-rev=origin/develop (rollout mode). +version: "2" + +run: + timeout: 5m + +linters: + default: none + enable: + - gosec # security-focused linter + - errcheck + - govet + - staticcheck + - ineffassign + - unused + exclusions: + rules: + - path: _test\.go + linters: + - gosec # test fixtures routinely trip G201/G404 etc. + +formatters: + enable: + - gofmt + - goimports diff --git a/knip.json b/knip.json new file mode 100644 index 000000000..c0c77c376 --- /dev/null +++ b/knip.json @@ -0,0 +1,31 @@ +{ + "$schema": "./node_modules/knip/schema.json", + "workspaces": { + ".": { + "entry": ["lefthook.yml"], + "ignoreDependencies": [ + "lefthook", + "knip", + "@ast-grep/cli", + "prettier", + "prettier-plugin-sort-json" + ] + }, + "desktop": {}, + "libraries/base": {}, + "server": { + "entry": [ + "app.vue", + "error.vue", + "nuxt.config.ts", + "plugins/**/*.{ts,js}", + "middleware/**/*.{ts,js}", + "layouts/**/*.vue" + ], + "ignore": [".nuxt/**", ".output/**", ".data/**", "build/**"], + "project": ["**/*.{ts,js,mjs,vue}"] + }, + "sites/docs": {}, + "sites/promo": {} + } +} diff --git a/lefthook.yml b/lefthook.yml new file mode 100644 index 000000000..c53c79373 --- /dev/null +++ b/lefthook.yml @@ -0,0 +1,74 @@ +# Lefthook git hooks for the Drop monorepo. +# Docs: https://lefthook.dev +# Escape hatches: git commit/push --no-verify, or LEFTHOOK=0 +# +# Commands with a glob that matches no staged files are skipped entirely, +# so e.g. a Markdown-only commit only runs prettier + gitleaks. + +pre-commit: + parallel: true + commands: + # Format only staged files, then re-stage them. + prettier: + glob: "*.{js,cjs,mjs,ts,tsx,vue,json,jsonc,css,scss,html,yaml,yml,md}" + exclude: + - "pnpm-lock.yaml" + - "**/generated/**" + run: pnpm exec prettier --write --ignore-unknown {staged_files} + stage_fixed: true + + # Scoped to server/ — the only package with an eslint flat config today. + # root: makes {staged_files} relative to server/, so the config resolves. + eslint: + root: server/ + glob: "server/**/*.{js,cjs,mjs,ts,tsx,vue}" + run: pnpm exec eslint --fix {staged_files} + stage_fixed: true + + # Structural lint rules (sgconfig.yml). Warnings only during rollout. + ast-grep: + glob: "*.{ts,tsx,vue,go}" + run: pnpm exec ast-grep scan {staged_files} + + # Secret scan on the diff about to be committed. Warns if the binary is + # missing locally — CI enforces this authoritatively (security.yml). + gitleaks: + run: | + if command -v gitleaks >/dev/null 2>&1; then + gitleaks protect --staged --redact --verbose + else + echo "::warning:: gitleaks not installed locally; CI enforces. Install: brew install gitleaks" + exit 0 + fi + +pre-push: + parallel: true + commands: + # Whole-project typecheck; gated on any server TS/Vue change. + typecheck: + root: server/ + glob: "server/**/*.{ts,tsx,vue}" + run: pnpm run typecheck + + # Per-crate clippy over changed crates only (-D warnings, matching droplet-ci). + # desktop/src-tauri is excluded locally (needs Tauri system libs); CI covers it. + clippy: + glob: "*.rs" + run: ./scripts/clippy-changed.sh + + # New-issues-only during rollout: backend/ has never been linted before. + # Compares against origin/develop; remove --new-from-rev once clean. + golangci-lint: + root: backend/ + glob: "backend/**/*.go" + run: | + base="$(git rev-parse --verify origin/develop 2>/dev/null || echo "")" + if [ -n "$base" ]; then + golangci-lint run --new-from-rev="$base" ./... + else + golangci-lint run ./... + fi + + # Report-only by design (rollout phase): prints findings, never fails. + knip: + run: pnpm exec knip --reporter compact || true diff --git a/package.json b/package.json index cdba65da0..ebe46540e 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,15 @@ { "name": "drop", "private": true, - "packageManager": "pnpm@10.33.0+sha512.10568bb4a6afb58c9eb3630da90cc9516417abebd3fabbe6739f0ae795728da1491e9db5a544c76ad8eb7570f5c4bb3d6c637b2cb41bfdcdb47fa823c8649319" -} \ No newline at end of file + "packageManager": "pnpm@10.33.0+sha512.10568bb4a6afb58c9eb3630da90cc9516417abebd3fabbe6739f0ae795728da1491e9db5a544c76ad8eb7570f5c4bb3d6c637b2cb41bfdcdb47fa823c8649319", + "devDependencies": { + "@ast-grep/cli": "^0.45.2", + "knip": "^6.32.2", + "lefthook": "^2.1.10", + "prettier": "^3.9.6", + "prettier-plugin-sort-json": "^4.2.0" + }, + "scripts": { + "prepare": "lefthook install" + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dbd7e40cb..a9abaf75e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -55,7 +55,23 @@ overrides: importers: - .: {} + .: + devDependencies: + '@ast-grep/cli': + specifier: ^0.45.2 + version: 0.45.2 + knip: + specifier: ^6.32.2 + version: 6.32.2 + lefthook: + specifier: ^2.1.10 + version: 2.1.10 + prettier: + specifier: ^3.9.6 + version: 3.9.6 + prettier-plugin-sort-json: + specifier: ^4.2.0 + version: 4.2.0(prettier@3.9.6) desktop: dependencies: @@ -90,7 +106,7 @@ importers: version: 9.39.4(jiti@2.7.0) nuxt: specifier: ^3.21.10 - version: 3.21.10(@electric-sql/pglite@0.4.1)(@oxc-project/types@0.140.0)(@parcel/watcher@2.5.6)(@types/node@24.13.3)(@vue/compiler-sfc@3.5.41)(cac@7.0.0)(db0@0.3.4(@electric-sql/pglite@0.4.1)(mysql2@3.15.3))(esbuild@0.28.1)(eslint@9.39.4(jiti@2.7.0))(ioredis@5.10.1)(lightningcss@1.33.0)(magicast@0.5.4)(mysql2@3.15.3)(optionator@0.9.4)(rolldown@1.1.5)(rollup-plugin-visualizer@7.0.1(rolldown@1.1.5)(rollup@4.62.4))(rollup@4.62.4)(sass@1.99.0)(terser@5.47.1)(typescript@5.9.3)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.99.0)(terser@5.47.1)(yaml@2.9.0))(vue-tsc@3.2.6(typescript@5.9.3))(yaml@2.9.0) + version: 3.21.10(@electric-sql/pglite@0.4.1)(@oxc-project/types@0.143.0)(@parcel/watcher@2.5.6)(@types/node@24.13.3)(@vue/compiler-sfc@3.5.41)(cac@7.0.0)(db0@0.3.4(@electric-sql/pglite@0.4.1)(mysql2@3.15.3))(esbuild@0.28.1)(eslint@9.39.4(jiti@2.7.0))(ioredis@5.10.1)(lightningcss@1.33.0)(magicast@0.5.4)(mysql2@3.15.3)(optionator@0.9.4)(rolldown@1.1.5)(rollup-plugin-visualizer@7.0.1(rolldown@1.1.5)(rollup@4.62.4))(rollup@4.62.4)(sass@1.99.0)(terser@5.47.1)(typescript@5.9.3)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.99.0)(terser@5.47.1)(yaml@2.9.0))(vue-tsc@3.2.6(typescript@5.9.3))(yaml@2.9.0) typescript: specifier: ^5.7.2 version: 5.9.3 @@ -144,7 +160,7 @@ importers: version: 4.2.2(vite@8.1.5(@types/node@22.19.17)(esbuild@0.28.0)(jiti@2.7.0)(sass@1.99.0)(terser@5.47.1)(yaml@2.9.0)) '@vueuse/nuxt': specifier: 13.6.0 - version: 13.6.0(magicast@0.5.2)(nuxt@3.21.10(@electric-sql/pglite@0.4.1)(@oxc-project/types@0.140.0)(@parcel/watcher@2.5.6)(@types/node@22.19.17)(@vue/compiler-sfc@3.5.41)(cac@6.7.14)(db0@0.3.4(@electric-sql/pglite@0.4.1)(mysql2@3.15.3))(esbuild@0.28.0)(eslint@9.39.4(jiti@2.7.0))(ioredis@5.10.1)(lightningcss@1.33.0)(magicast@0.5.2)(mysql2@3.15.3)(optionator@0.9.4)(rolldown@1.1.5)(rollup-plugin-visualizer@7.0.1(rolldown@1.1.5)(rollup@4.62.4))(rollup@4.62.4)(sass@1.99.0)(terser@5.47.1)(typescript@5.9.3)(vite@8.1.5(@types/node@22.19.17)(esbuild@0.28.0)(jiti@2.7.0)(sass@1.99.0)(terser@5.47.1)(yaml@2.9.0))(vue-tsc@3.2.6(typescript@5.9.3))(yaml@2.9.0))(vue@3.5.41(typescript@5.9.3)) + version: 13.6.0(magicast@0.5.2)(nuxt@3.21.10(@electric-sql/pglite@0.4.1)(@oxc-project/types@0.143.0)(@parcel/watcher@2.5.6)(@types/node@22.19.17)(@vue/compiler-sfc@3.5.41)(cac@6.7.14)(db0@0.3.4(@electric-sql/pglite@0.4.1)(mysql2@3.15.3))(esbuild@0.28.0)(eslint@9.39.4(jiti@2.7.0))(ioredis@5.10.1)(lightningcss@1.33.0)(magicast@0.5.2)(mysql2@3.15.3)(optionator@0.9.4)(rolldown@1.1.5)(rollup-plugin-visualizer@7.0.1(rolldown@1.1.5)(rollup@4.62.4))(rollup@4.62.4)(sass@1.99.0)(terser@5.47.1)(typescript@5.9.3)(vite@8.1.5(@types/node@22.19.17)(esbuild@0.28.0)(jiti@2.7.0)(sass@1.99.0)(terser@5.47.1)(yaml@2.9.0))(vue-tsc@3.2.6(typescript@5.9.3))(yaml@2.9.0))(vue@3.5.41(typescript@5.9.3)) argon2: specifier: ^0.43.0 version: 0.43.1 @@ -198,7 +214,7 @@ importers: version: 8.1.1 nuxt: specifier: ^3.21.10 - version: 3.21.10(@electric-sql/pglite@0.4.1)(@oxc-project/types@0.140.0)(@parcel/watcher@2.5.6)(@types/node@22.19.17)(@vue/compiler-sfc@3.5.41)(cac@6.7.14)(db0@0.3.4(@electric-sql/pglite@0.4.1)(mysql2@3.15.3))(esbuild@0.28.0)(eslint@9.39.4(jiti@2.7.0))(ioredis@5.10.1)(lightningcss@1.33.0)(magicast@0.5.2)(mysql2@3.15.3)(optionator@0.9.4)(rolldown@1.1.5)(rollup-plugin-visualizer@7.0.1(rolldown@1.1.5)(rollup@4.62.4))(rollup@4.62.4)(sass@1.99.0)(terser@5.47.1)(typescript@5.9.3)(vite@8.1.5(@types/node@22.19.17)(esbuild@0.28.0)(jiti@2.7.0)(sass@1.99.0)(terser@5.47.1)(yaml@2.9.0))(vue-tsc@3.2.6(typescript@5.9.3))(yaml@2.9.0) + version: 3.21.10(@electric-sql/pglite@0.4.1)(@oxc-project/types@0.143.0)(@parcel/watcher@2.5.6)(@types/node@22.19.17)(@vue/compiler-sfc@3.5.41)(cac@6.7.14)(db0@0.3.4(@electric-sql/pglite@0.4.1)(mysql2@3.15.3))(esbuild@0.28.0)(eslint@9.39.4(jiti@2.7.0))(ioredis@5.10.1)(lightningcss@1.33.0)(magicast@0.5.2)(mysql2@3.15.3)(optionator@0.9.4)(rolldown@1.1.5)(rollup-plugin-visualizer@7.0.1(rolldown@1.1.5)(rollup@4.62.4))(rollup@4.62.4)(sass@1.99.0)(terser@5.47.1)(typescript@5.9.3)(vite@8.1.5(@types/node@22.19.17)(esbuild@0.28.0)(jiti@2.7.0)(sass@1.99.0)(terser@5.47.1)(yaml@2.9.0))(vue-tsc@3.2.6(typescript@5.9.3))(yaml@2.9.0) nuxt-security: specifier: 2.2.0 version: 2.2.0(magicast@0.5.2)(rollup@4.62.4) @@ -265,7 +281,7 @@ importers: version: 4.3.0(eslint@9.39.4(jiti@2.7.0))(jsonc-eslint-parser@2.4.2)(vue-eslint-parser@10.4.1(eslint@9.39.4(jiti@2.7.0)))(yaml-eslint-parser@1.3.2) '@nuxt/eslint': specifier: ^1.3.0 - version: 1.15.2(@typescript-eslint/utils@8.58.2(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(@vue/compiler-sfc@3.5.41)(eslint-import-resolver-node@0.3.10)(eslint@9.39.4(jiti@2.7.0))(magic-string@1.1.0)(magicast@0.5.2)(oxc-parser@0.140.0)(rolldown@1.1.5)(typescript@5.9.3)(unplugin@3.3.0(esbuild@0.28.0)(rolldown@1.1.5)(rollup@4.62.4)(vite@8.1.5(@types/node@22.19.17)(esbuild@0.28.0)(jiti@2.7.0)(sass@1.99.0)(terser@5.47.1)(yaml@2.9.0)))(vite@8.1.5(@types/node@22.19.17)(esbuild@0.28.0)(jiti@2.7.0)(sass@1.99.0)(terser@5.47.1)(yaml@2.9.0)) + version: 1.15.2(@typescript-eslint/utils@8.58.2(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(@vue/compiler-sfc@3.5.41)(eslint-import-resolver-node@0.3.10)(eslint@9.39.4(jiti@2.7.0))(magic-string@1.1.0)(magicast@0.5.2)(oxc-parser@0.143.0)(rolldown@1.1.5)(typescript@5.9.3)(unplugin@3.3.0(esbuild@0.28.0)(rolldown@1.1.5)(rollup@4.62.4)(vite@8.1.5(@types/node@22.19.17)(esbuild@0.28.0)(jiti@2.7.0)(sass@1.99.0)(terser@5.47.1)(yaml@2.9.0)))(vite@8.1.5(@types/node@22.19.17)(esbuild@0.28.0)(jiti@2.7.0)(sass@1.99.0)(terser@5.47.1)(yaml@2.9.0)) '@tailwindcss/forms': specifier: ^0.5.9 version: 0.5.11(tailwindcss@4.2.2) @@ -307,7 +323,7 @@ importers: version: 1.15.11 nitropack: specifier: ^2.13.4 - version: 2.13.4(@electric-sql/pglite@0.4.1)(mysql2@3.15.3)(oxc-parser@0.140.0)(rolldown@1.1.5)(vite@8.1.5(@types/node@22.19.17)(esbuild@0.28.0)(jiti@2.7.0)(sass@1.99.0)(terser@5.47.1)(yaml@2.9.0)) + version: 2.13.4(@electric-sql/pglite@0.4.1)(mysql2@3.15.3)(oxc-parser@0.143.0)(rolldown@1.1.5)(vite@8.1.5(@types/node@22.19.17)(esbuild@0.28.0)(jiti@2.7.0)(sass@1.99.0)(terser@5.47.1)(yaml@2.9.0)) ofetch: specifier: ^1.4.1 version: 1.5.1 @@ -469,6 +485,55 @@ packages: '@ark/util@0.56.0': resolution: {integrity: sha512-BghfRC8b9pNs3vBoDJhcta0/c1J1rsoS1+HgVUreMFPdhz/CRAKReAu57YEllNaSy98rWAdY1gE+gFup7OXpgA==} + '@ast-grep/cli-darwin-arm64@0.45.2': + resolution: {integrity: sha512-UeLO9dAyWVesDII8m545LEzooNLRmNigbv6Qo7bACkB3jYTqoVbLH96+PZFFbJcQRcabqiwOiDlxhodkCMkP5A==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@ast-grep/cli-darwin-x64@0.45.2': + resolution: {integrity: sha512-Y9sUYEGx/jE/1e9bq8E11AIRDSTMj7ZCzfK862TVM2yc+jKWif+IcqPkDNX4rpz+erTxK+IsSVwoEXduB+zSPg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@ast-grep/cli-linux-arm64-gnu@0.45.2': + resolution: {integrity: sha512-hfxdLxxCcGobue++cWNf7O/VC+Ctj9FeFqgTkKUxxZbPjMawHLNvd2+uXbTimgOOZrAZqm2VsZ9CRxw8zrADIA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@ast-grep/cli-linux-x64-gnu@0.45.2': + resolution: {integrity: sha512-Fh/evRRyJaH0Dctk+JMN4o7dz0tI5Pj3XJUDlp/aAwvXnZ0kuOsAMpO4tlYYq3By0/6GbKCr2Qau1t9cx1rMVQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@ast-grep/cli-win32-arm64-msvc@0.45.2': + resolution: {integrity: sha512-wzlAX2K9255DNrHMpK9EpM+61ITYpTL14P47eZ4or2jYGUpB0bl1r7FjTC+eDXcwHLJozvu1b6f42iy0505JvA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@ast-grep/cli-win32-ia32-msvc@0.45.2': + resolution: {integrity: sha512-hgRpvkX2n8ih3Fywj6dn6ekLmKI0ytC2HdDlLvAQBDzxYzxVMpb6mydF+eR4lqPEGTSuGXmN7lsbDzRRFX7qqQ==} + engines: {node: '>= 10'} + cpu: [ia32] + os: [win32] + + '@ast-grep/cli-win32-x64-msvc@0.45.2': + resolution: {integrity: sha512-IFV/nygDDDOecuyqaaHIRwhEgVu8+42jm9UIiiZ/P9zUxDMZa34Z440FFGqqRvf4Mnm+1X0jRsNRxGyWPpzPog==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@ast-grep/cli@0.45.2': + resolution: {integrity: sha512-euUPqWTyWt6Ma9WBRCMM12UZXkdQAoUVyXFWnPXtFfIuiF3oJUhEWpzlcSaliiFcXLfzSuMu4rAhhtlGrTEaZA==} + engines: {node: '>= 12.0.0'} + hasBin: true + '@astrojs/compiler-binding-darwin-arm64@0.3.1': resolution: {integrity: sha512-IEmEF2fUIlTHtpeE/isyEGVOB14cEyh/LZOFYt6wn3jNyVpdC8aR5OZ+RzFUR/f+8ZDM1LaMwZKvoA7eMyJeFw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -2811,18 +2876,36 @@ packages: cpu: [arm] os: [android] + '@oxc-parser/binding-android-arm-eabi@0.143.0': + resolution: {integrity: sha512-n9uozULWflPqBtdmI8lAabLqGKNgLVNN0ZH8HfgCwpKGNtzRzauB76jTiW/3YLkcA7N1zskpi9GdVnZuu1SAvg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + '@oxc-parser/binding-android-arm64@0.140.0': resolution: {integrity: sha512-Ia8jSvikUX6Sf+Ht+KOCUF/k1HpR0VlmqIYymubmWDebOEGtsyliHDR6JxsZ4IX3/c/GbrB1uh09aVGQv/LQmQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] + '@oxc-parser/binding-android-arm64@0.143.0': + resolution: {integrity: sha512-9BbdjHETk6O3zH/DDid9IgBtF0GlpLabNKN231uraXpRDSfY+iiZxTP5bk1Z63GBownVdhdINFIeddmMz4MzpQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + '@oxc-parser/binding-darwin-arm64@0.140.0': resolution: {integrity: sha512-G6VK0nK61pH0d0mBjUqSZbVxGqqO5uzeginLDQj+gOO6ObfJjXRwgkD/ol0w1INcnFeAb6YGGO7qc3ueGHaycQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] + '@oxc-parser/binding-darwin-arm64@0.143.0': + resolution: {integrity: sha512-gh+6ecoHUy4/sUcolBl/1qPXKBbYNxFY0Pk0ujgQvINTMSftJY7o4yb8gOkDJPeZeB8+a+u7xTe6umoP8N5HFA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + '@oxc-parser/binding-darwin-arm64@0.70.0': resolution: {integrity: sha512-pIi7L9PnsBctS/ruW6JQVSYRJkh76PblBN46uQxpBfVsM57c1s4HGZlmGysQWbdmQTFDZW+SmH3u0JpmDLF0+A==} engines: {node: '>=14.0.0'} @@ -2835,6 +2918,12 @@ packages: cpu: [x64] os: [darwin] + '@oxc-parser/binding-darwin-x64@0.143.0': + resolution: {integrity: sha512-qd1hl2d+lXgHv/VQ/M9qm8TrMC5T4RqDBwtOnl+1D0QMjwcz+8AaB4JSg8STgeag0GP6a6L74XEGAsrTSJWNzQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + '@oxc-parser/binding-darwin-x64@0.70.0': resolution: {integrity: sha512-EbKqtOHzZR56ZFC5HHg6XrYneFAJmpLC1Z6FSgbI061Ley1atAViQg7S6Agm9wAcPpns+BeFJqXEBx/y3MKa2w==} engines: {node: '>=14.0.0'} @@ -2847,6 +2936,12 @@ packages: cpu: [x64] os: [freebsd] + '@oxc-parser/binding-freebsd-x64@0.143.0': + resolution: {integrity: sha512-M5XXcNa7aOqLPKTR41msfghKu2yQ4xWvCm11/gwU0JzOzHNk5sgW//rVEjJ+LO48+VDAMzXTSzurUVxIDKwozw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + '@oxc-parser/binding-freebsd-x64@0.70.0': resolution: {integrity: sha512-MVUaOMEUVE8q3nsWtEo589h++V5wAdqTbCRa9WY4Yuyxska4xcuJQk/kDNCx+n92saS7Luk+b20O9+VCI03c+A==} engines: {node: '>=14.0.0'} @@ -2859,6 +2954,12 @@ packages: cpu: [arm] os: [linux] + '@oxc-parser/binding-linux-arm-gnueabihf@0.143.0': + resolution: {integrity: sha512-T/GXusuOkPNQhCQCSBbcU/N8j0rAypuDBl1IyFK+lyYT594XsVz80clPC/OtbSSpBGyJxj8uYEfctxVuxVYoww==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + '@oxc-parser/binding-linux-arm-gnueabihf@0.70.0': resolution: {integrity: sha512-8N4JTYTgKiRHlMUDAdzKs6iEC57a8ex408VgKoLD/Fl+Un79qOti3S9sotdnWSdH/BsDQeO5NW+PKaqFBTw+hA==} engines: {node: '>=14.0.0'} @@ -2871,6 +2972,12 @@ packages: cpu: [arm] os: [linux] + '@oxc-parser/binding-linux-arm-musleabihf@0.143.0': + resolution: {integrity: sha512-oKu4RcBlXSqo3OC62dp6YTnQaZIurNDpCX3BnAM3+bJxt7s8J2TJKMnC0UYer1qhlRaDCg6wkTaTw+2IlsZ12w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + '@oxc-parser/binding-linux-arm-musleabihf@0.70.0': resolution: {integrity: sha512-Bsu+YvtgWuSfSDJTHMF5APZBOtvddR0GiHyrL0yaXDwaYvAL/E7XcoSK2GdmKTpw+J8nk5IlejEXlQliPo52pQ==} engines: {node: '>=14.0.0'} @@ -2884,6 +2991,13 @@ packages: os: [linux] libc: [glibc] + '@oxc-parser/binding-linux-arm64-gnu@0.143.0': + resolution: {integrity: sha512-WJBbD186AZmMGaSIhlktC+rPl8L3peCTXAh88Ih9uEvK0en2mPojGyCGYiL6mHtV1RPV3JyfJW5t6n5hh0lXhA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + '@oxc-parser/binding-linux-arm64-gnu@0.70.0': resolution: {integrity: sha512-tDzHWKexJPHR+qSiuAFoZ1v8EgCd4ggBNbjJHkcIHsoYKnsKaT1+uE9xfW9UhI1mhv2lo1JJ9n9og2yDTGxSeA==} engines: {node: '>=14.0.0'} @@ -2898,6 +3012,13 @@ packages: os: [linux] libc: [musl] + '@oxc-parser/binding-linux-arm64-musl@0.143.0': + resolution: {integrity: sha512-t1AcYOwEzgceadT4v5e+vaCCb0AncCA3v5AyzfBAz/tMq11qzVccXKzNHtkWdjBsgvTKwRkaUF3QvT4kot8vcQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + '@oxc-parser/binding-linux-arm64-musl@0.70.0': resolution: {integrity: sha512-BJ+N25UWmHU624558ojSTnht3uFL00jV1c8qk1hnKf4cl6+ovFcoktRWAWSBlgLEP8tLlu8qgIhz875tMj2PkQ==} engines: {node: '>=14.0.0'} @@ -2912,6 +3033,13 @@ packages: os: [linux] libc: [glibc] + '@oxc-parser/binding-linux-ppc64-gnu@0.143.0': + resolution: {integrity: sha512-RsnO/NoD8376LMJq8JS8TwI0ieNaFRTuNe2GVJntQg6gwZNMENZsEbknHdVwjpOmxdGLGodcwaGSbAeRr5Bgjw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + '@oxc-parser/binding-linux-riscv64-gnu@0.140.0': resolution: {integrity: sha512-A1x+PMWZmSGaFVOx2YeNTFau8uD+QO14/vLP4GrcuvUPs3+nBkUOjy9Lus86ftHsDojjYMbvBelmKc3F7Rv08g==} engines: {node: ^20.19.0 || >=22.12.0} @@ -2919,6 +3047,13 @@ packages: os: [linux] libc: [glibc] + '@oxc-parser/binding-linux-riscv64-gnu@0.143.0': + resolution: {integrity: sha512-48fSVfR9TZi5CASZFyv0VC6z6BCoeihFsX031mAD/oSH7d9PYsPgIqza7d9mjP7Z2KTEpTFyH6SIu0Ui6R1vdg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [glibc] + '@oxc-parser/binding-linux-riscv64-gnu@0.70.0': resolution: {integrity: sha512-nxu22nVuPA2xy1cxvBC0D5mVl0myqStOw3XBkVkDViNL01iPyuEFJd5VsM0GqsgrXvF95H/jrbMd+XWnto924g==} engines: {node: '>=14.0.0'} @@ -2933,6 +3068,13 @@ packages: os: [linux] libc: [musl] + '@oxc-parser/binding-linux-riscv64-musl@0.143.0': + resolution: {integrity: sha512-T8CpdD+SfE01DnIOD4HpVxu0ZJOfMJ/VhCvikKfaXAxkZ+9veyLM/D2hpi7Y2hFUyPmVQO3FNZHmYzV/WlVR4g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [musl] + '@oxc-parser/binding-linux-s390x-gnu@0.140.0': resolution: {integrity: sha512-2M1DPm/8w9I//YzFlFC9qXw+r2tJFh5CYwRlYTq2vUJQS7qoQftEDeCZ8EnN7KHtvSiXvYj8mZI5pR7DpXmcEw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -2940,6 +3082,13 @@ packages: os: [linux] libc: [glibc] + '@oxc-parser/binding-linux-s390x-gnu@0.143.0': + resolution: {integrity: sha512-QLdeMsCcacenPEFsfxnBUDF1y6opyz5+fmOz9bfD5Y7fiGCMupUCuB3KTPQhNwshIG1P9fPqar9MHxuBDd4bwQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + '@oxc-parser/binding-linux-s390x-gnu@0.70.0': resolution: {integrity: sha512-AQ6Xj97lYRxHZl94cZIHJxT5M1qkeEi+vQe+e7M2lAtjcURl8cwhZmWKSv4rt4BQRVfO3ys0bY8AgIh4eFJiqw==} engines: {node: '>=14.0.0'} @@ -2954,6 +3103,13 @@ packages: os: [linux] libc: [glibc] + '@oxc-parser/binding-linux-x64-gnu@0.143.0': + resolution: {integrity: sha512-659ujfqLy6k7cuH3sbzhd8b+ztSq+i6E2E9pG78Q0BmHjAExfGIdgc8cGgMdwAozDXeZFHkJ+LXYJdWsaGdgyw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + '@oxc-parser/binding-linux-x64-gnu@0.70.0': resolution: {integrity: sha512-RIxaVsIxtG90CoX6/Okij8itaMrJp4SEJm1pSL0pz3hGo0yur3Il9M1mmGvOpW+avY8uHdwXIvf2qMnnTKZuoQ==} engines: {node: '>=14.0.0'} @@ -2968,6 +3124,13 @@ packages: os: [linux] libc: [musl] + '@oxc-parser/binding-linux-x64-musl@0.143.0': + resolution: {integrity: sha512-/Mw/9j4TfZcnKphPrzOE6t4MMknXadcAAuVUlDRTF/ETWB5xOgQvOJV2Mh9We/bWxZdoxaGAdc+hy4GuYwQ2yQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + '@oxc-parser/binding-linux-x64-musl@0.70.0': resolution: {integrity: sha512-B3S0G4TlZ+WLdQq4mSQtt2ZW0MAkKWc8dla17tZY86kcXvvCWwACvj7I27Z/nSlb7uJOdRZS9/r6Gw0uAARNVQ==} engines: {node: '>=14.0.0'} @@ -2981,6 +3144,12 @@ packages: cpu: [arm64] os: [openharmony] + '@oxc-parser/binding-openharmony-arm64@0.143.0': + resolution: {integrity: sha512-8rIKWR2BFuifbIK/1XB9wTaSdtuJ25dlE7ZQYDnEwj/2xH2vHsxnvIjHT3ZjSVuLLwGGlSslIG/fbOJ8TV8rTw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + '@oxc-parser/binding-wasm32-wasi@0.140.0': resolution: {integrity: sha512-vFiC1hqys+hkX1GnQkIoiTQJNiUm43Z0lO35ETKXTw0YtpW7+cN58YRRXFAQQ+TgpkIi3lrhcxdlnqz+Oi3ptQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -2997,6 +3166,12 @@ packages: cpu: [arm64] os: [win32] + '@oxc-parser/binding-win32-arm64-msvc@0.143.0': + resolution: {integrity: sha512-5U9kQYMfRRI6Zq7KDxgbIP0RMnKrfn3gLepRMgJuRkPSUALTiRCk9d/uyhb4lGDjUdzwK7mBkKqhLgzBPCmLpQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + '@oxc-parser/binding-win32-arm64-msvc@0.70.0': resolution: {integrity: sha512-6k8/s78g0GQKqrxk4F0wYj32NBF9oSP6089e6BeuIRQ9l+Zh0cuI6unJeLzXNszxmlqq84xmf/tmP3MSDG43Uw==} engines: {node: '>=14.0.0'} @@ -3009,12 +3184,24 @@ packages: cpu: [ia32] os: [win32] + '@oxc-parser/binding-win32-ia32-msvc@0.143.0': + resolution: {integrity: sha512-25P7AaHk4R88Yv2XH4gToDVmh0cOu+bEURQU10CRrmvgabfRArSGAP5osmwUKeSUHj0VS50upbpbRWWW/m7mHA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ia32] + os: [win32] + '@oxc-parser/binding-win32-x64-msvc@0.140.0': resolution: {integrity: sha512-kHbE1zWyb5OQgJA6/5P4WjiuB01sYdQwtZnSSyE58FQEXDAMnyeeq4vj7KgN75i5SlBzOs8A5MrtlD3gOlDKqQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] + '@oxc-parser/binding-win32-x64-msvc@0.143.0': + resolution: {integrity: sha512-ORMh3JE1s6V7ySicdRK7vgaDQnn5o+UHg9ct989PlWHbel8O9ARrmWXM6kZjrBMtNucxNayQ8g69G0VfWzhANw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + '@oxc-parser/binding-win32-x64-msvc@0.70.0': resolution: {integrity: sha512-nd9o1QtEvupaJZ3Wn7PfsuC00n31NNRQZ5+Mui6Q0ZyDzp+obqPUSbSt7xh9Dy0c5zgtYMk8WY4n/VBJY2VvTQ==} engines: {node: '>=14.0.0'} @@ -3031,12 +3218,118 @@ packages: '@oxc-project/types@0.140.0': resolution: {integrity: sha512-h5LUOzGArYemnW1NMz/DuuQhBi96J6JL2Bk8zE4kvqxB5Sg3jxmCiH4uyOWHDkiKSt5vWlG4FIwCR/DbstcNRQ==} + '@oxc-project/types@0.143.0': + resolution: {integrity: sha512-u6JZdLBTLotrNC9Vd6vPssINdzcCzleKAH6EJKImQb7GtYvX5keN2dxkoK44stCc4tffE6QQRtZTXVSzsLUlWA==} + '@oxc-project/types@0.60.0': resolution: {integrity: sha512-prhfNnb3ATFHOCv7mzKFfwLij5RzoUz6Y1n525ZhCEqfq5wreCXL+DyVoq3ShukPo7q45ZjYIdjFUgjj+WKzng==} '@oxc-project/types@0.70.0': resolution: {integrity: sha512-ngyLUpUjO3dpqygSRQDx7nMx8+BmXbWOU4oIwTJFV2MVIDG7knIZwgdwXlQWLg3C3oxg1lS7ppMtPKqKFb7wzw==} + '@oxc-resolver/binding-android-arm-eabi@11.24.2': + resolution: {integrity: sha512-y09e0L0SRI2OA2tUIrjBgoV3eH5hvUKXNkJqXmNo5V2WxIjyC7I7aJfRLMEVpA8yi95f90gFDvO0VMgrDw+vwA==} + cpu: [arm] + os: [android] + + '@oxc-resolver/binding-android-arm64@11.24.2': + resolution: {integrity: sha512-cl4icWaZFnLdg8m6qtnh5rBMuGbxc/ptStFHLeCNwr+2cZjkjNwQu/jYRS0CHlnPecOJMpuS5M6/BH+0J/YkEg==} + cpu: [arm64] + os: [android] + + '@oxc-resolver/binding-darwin-arm64@11.24.2': + resolution: {integrity: sha512-At29QEMF6HajbQvgY8K6OXnHD1x9rad74xBEfmCB6ZqCGsdq75aK7tOYcTbOanMy8qdIBrfL3SMr3p/lfSlb9w==} + cpu: [arm64] + os: [darwin] + + '@oxc-resolver/binding-darwin-x64@11.24.2': + resolution: {integrity: sha512-A5Kqr1EUj4oIL5CF4WRssq/o5P0Y11cwoFouMRmQ7YnC/A8V93nv1nb7aSU8HwcgmXropjLNkVTl4MN87cu28Q==} + cpu: [x64] + os: [darwin] + + '@oxc-resolver/binding-freebsd-x64@11.24.2': + resolution: {integrity: sha512-R5xkRBRRz7ceH/P5Jrc6G7FmdUdgpLYyESFAUDVTNQ9K0sGPxcp4ljiwEwEqsvNcQ4sYbMRrWcHHBCu7ksAJVw==} + cpu: [x64] + os: [freebsd] + + '@oxc-resolver/binding-linux-arm-gnueabihf@11.24.2': + resolution: {integrity: sha512-k/RuYL4L/R58IBn3wT5ma3Wh4k62bp1eYCFRWCmMsasUOqL+H6sW0VGFadEzKWXFFlz+2uIMoeMk9ySSZJHgbg==} + cpu: [arm] + os: [linux] + + '@oxc-resolver/binding-linux-arm-musleabihf@11.24.2': + resolution: {integrity: sha512-bnHAak3ujYfH5pKk4NieFNbvYvernfoQDgwLddbZ3OtMYrem87/qjlA+u+aKG0oZcqSLGCful/6/CEA+aeAgaA==} + cpu: [arm] + os: [linux] + + '@oxc-resolver/binding-linux-arm64-gnu@11.24.2': + resolution: {integrity: sha512-vDT3KHgzYp47gmtNOqL2VNhCyl5Zv643eyxm//A68J8DeUGXrvD1pZFiaT4jSfe+RInfnn1R2yVHye4enx6RnA==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@oxc-resolver/binding-linux-arm64-musl@11.24.2': + resolution: {integrity: sha512-+kMlQvbzfyEYtu5FcjE4p+ttBLpKW4d/AsAsuE69BxV6V4twZJeIQZFfD8gh/wqglY0MkPSezWXQH0jBV13MUw==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@oxc-resolver/binding-linux-ppc64-gnu@11.24.2': + resolution: {integrity: sha512-shjfMhmZ3gq9fv/w7bi3PnZlgOPG+2QAOFf0BJF0EgBSIGZ6PMLN2zbGEblTUYB/NKVDRyYhE2ff3dJ1QqNPkA==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@oxc-resolver/binding-linux-riscv64-gnu@11.24.2': + resolution: {integrity: sha512-zGelwFR5oRo+b69k8Lrzun86DyUHzfKN6cnjbR9l7Z7NIRznOE/2ZvPa1IUKqAL2PzAXOdwkfVqNvO1H2RlpAw==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@oxc-resolver/binding-linux-riscv64-musl@11.24.2': + resolution: {integrity: sha512-qxZ1SWCXJY0eyhAlP6Lmo9F2Nrtx7EkYj9oCgL8apDPCwXwCEDA2U697bbT81JIc2IrVjxO4KX6WU2N+oN9Z4w==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@oxc-resolver/binding-linux-s390x-gnu@11.24.2': + resolution: {integrity: sha512-sGCecF3cx2DFlH4t/z7ApnOnXqN48p5p5mlHDEnHTAukQa2P+qMVE4CwyWE9W+q/m3QJ7kKfGrIjax31f44oFQ==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@oxc-resolver/binding-linux-x64-gnu@11.24.2': + resolution: {integrity: sha512-k/VlMMcSzMlahb3/fENM4rTlsJ0s3fFROA0KXPBmKggqmTSaE383sl8F3KCOXPLmVsYfW6hCitMhXCEtNeZxxg==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@oxc-resolver/binding-linux-x64-musl@11.24.2': + resolution: {integrity: sha512-8hbnZyNi97b/8wapYaIF9+t9GmZKBW2vunaOc3h9HGJptH7b7XpvZqOTBSm/MpTjr7H497BlgOaSfLUdhmy2bw==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@oxc-resolver/binding-openharmony-arm64@11.24.2': + resolution: {integrity: sha512-MvyGik3a6pVgZ0t/kWlbmFxFLmXQJwgLsY2eYFHLpy0wGwRbfzeIGgDwQ3kXqE30z+kSXennRkCrT7TUvkptNg==} + cpu: [arm64] + os: [openharmony] + + '@oxc-resolver/binding-wasm32-wasi@11.24.2': + resolution: {integrity: sha512-vHcssMPwO08RTvj/c0iOBz90attxyG3wQJ0dTcyEQK43LRpcdLWZlV5feBhv6Isn6ahbQIzHbCgfa81+RiML0Q==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@oxc-resolver/binding-win32-arm64-msvc@11.24.2': + resolution: {integrity: sha512-uokJqro2iBqkFvJdKQLP7d8/BUmFwESQFVmIJUQKj1Xn1a/LysJoe1vmeECLF5b3jsV8CAL5sEMJXX6SdK9Nhg==} + cpu: [arm64] + os: [win32] + + '@oxc-resolver/binding-win32-x64-msvc@11.24.2': + resolution: {integrity: sha512-UqGPmo56KDfLlfXFAFIrNflHT8tFxWGEivWg3Zeyp4Uy2NlKN1FGPr6/BxcLGG3+kZ6Wp14g5Uj+n71boqZfiw==} + cpu: [x64] + os: [win32] + '@oxc-transform/binding-android-arm-eabi@0.141.0': resolution: {integrity: sha512-FOdseb5KpV3JtkM+7TN4u3sW3aQkUSRy0bWWiKgT/qrIGqZHqzKEUpwaMybutsfwEglWBXuJgnrT9loWisPkrw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -6954,6 +7247,9 @@ packages: fault@2.0.1: resolution: {integrity: sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ==} + fd-package-json@2.0.0: + resolution: {integrity: sha512-jKmm9YtsNXN789RS/0mSzOC1NUq9mkVd65vbSSVsKdjGvYXBuE4oWe2QOEoFeRmJg+lPuZxpmrfFclNhoRMneQ==} + fd-slicer@1.1.0: resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==} @@ -7100,6 +7396,11 @@ packages: resolution: {integrity: sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==} engines: {node: '>=0.4.x'} + formatly@0.3.0: + resolution: {integrity: sha512-9XNj/o4wrRFyhSMJOvsuyMwy8aUfBaZ1VrqHVfohyXf0Sw0e+yfKG+xZaY3arGCOMdwFsqObtzVOc1gU9KiT9w==} + engines: {node: '>=18.3.0'} + hasBin: true + fraction.js@5.3.4: resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==} @@ -7233,6 +7534,9 @@ packages: get-tsconfig@4.14.0: resolution: {integrity: sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==} + get-tsconfig@4.14.1: + resolution: {integrity: sha512-Dz/6HxkrxgNehhxLVeyv8sad9UzF2xBVeaKBQNDfJ5XiSXmp2gTR0eO0RWiT2NCKS5aGP9jjkOMggTN90qU50A==} + get-tsconfig@5.0.0-beta.4: resolution: {integrity: sha512-7nF7C9fIPFEMHgEMEfgIlO9wDdZ8CyHw27rWciFZfHvHDReIiPhsYuzPRXsfvBCqFy1l8RRyyWV7QLM+ZhUJsQ==} engines: {node: '>=20.20.0'} @@ -8195,6 +8499,11 @@ packages: resolution: {integrity: sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==} engines: {node: '>= 8'} + knip@6.32.2: + resolution: {integrity: sha512-WXTXbmocrw7gqm1A1TQvFN0OgJ7hUSU6E1g6SPRIzzHFogUBhXByc7cYeOFVtJ2uODg7DP4VbESYBYnfbtBYsg==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + knitwork@1.3.0: resolution: {integrity: sha512-4LqMNoONzR43B1W0ek0fhXMsDNW/zxa1NdFAVMY+k28pgZLovR4G3PB5MrpTxCy1QaZCqNoiaKPr5w5qZHfSNw==} @@ -8216,6 +8525,60 @@ packages: resolution: {integrity: sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==} engines: {node: '>= 0.6.3'} + lefthook-darwin-arm64@2.1.10: + resolution: {integrity: sha512-nw+X8wRNDoUUV6WSteyKBbcLySq+fsmZt5WV/s50ZJpysmsDKJOUMln6SllNfP+60dzUahAO7REco/2633BsLg==} + cpu: [arm64] + os: [darwin] + + lefthook-darwin-x64@2.1.10: + resolution: {integrity: sha512-KQ/bHmvpkFdHMn4pZnUdTf+GuSC+aBBgBTxZT4GW+6cSf+qbErKZBhK7cH6BmILsvx43+VzEArvHYY7YOfRFOQ==} + cpu: [x64] + os: [darwin] + + lefthook-freebsd-arm64@2.1.10: + resolution: {integrity: sha512-8su6DwydP7+pv7kG0zCtjphqsw4ouOnfexRUErapy5GTxYBoUOhYz3RSHTSWNRsK6W4jva7FPUh2Lp5/PSn30w==} + cpu: [arm64] + os: [freebsd] + + lefthook-freebsd-x64@2.1.10: + resolution: {integrity: sha512-GeAJEFxko3Lk+AsnS3NleAFrpyMLFUKOlgJvPKuU0xHwVEI/z+ZoCcmuO0BX+4CS0NLbZhC/YQAvBASqDvvVdQ==} + cpu: [x64] + os: [freebsd] + + lefthook-linux-arm64@2.1.10: + resolution: {integrity: sha512-1sHTCmpTWjVMs+yKPBLRNT1kuuIr1yjietlk7rCB6wFPVOS6Ph3o2zPFH2AvW1UymHlqwyHXzBr9EtDpQ7j1mQ==} + cpu: [arm64] + os: [linux] + + lefthook-linux-x64@2.1.10: + resolution: {integrity: sha512-z/VlRB3bh6mBvW3r1rwnJ5vP8z+Krx5gJzkZ4veDXh+6FlRTx8wtd3g3fllOv/yZMxkgmL3fQoFXv05Esa7vBQ==} + cpu: [x64] + os: [linux] + + lefthook-openbsd-arm64@2.1.10: + resolution: {integrity: sha512-430zL8sSIKw5P0YXGG6PB+eAhHa06n0PXuaERaAQE4Ss3odfqwnl5Mq9hQmkEnOS1EGiQEKkd0UHv/i4PtMNIQ==} + cpu: [arm64] + os: [openbsd] + + lefthook-openbsd-x64@2.1.10: + resolution: {integrity: sha512-bgkO8PphGZVDhQgCJ524aYYPI5491pVmCiLPGjBIo1AvOSlIyw4N1Y+1C3QfqwEmechzw+Aq16SNc8pqv6UuXg==} + cpu: [x64] + os: [openbsd] + + lefthook-windows-arm64@2.1.10: + resolution: {integrity: sha512-5Q6etF0Fla2DDA4ilDySrdNgiR5+W7cJZwnZ69Je3kvWCaWm4wnkuc8FEdjp3kiL2x3ZXipdI00f5vpO8aWmog==} + cpu: [arm64] + os: [win32] + + lefthook-windows-x64@2.1.10: + resolution: {integrity: sha512-c/XH8YZtylG4XaxzqFfXluvq2LXq2W/p54Bnzn3+Z7E5X2Fk3JlFJAibulMbIt2+w8T7UI/r97ok5GqE4kGaeA==} + cpu: [x64] + os: [win32] + + lefthook@2.1.10: + resolution: {integrity: sha512-K7mM4WoqMwqfXYK11EHy+lSH1uW8XHni3Yn/bSqyerPkUPygGdf3xn18JoV5HyA06xuQL3ofGAOjG01QX9oJ4w==} + hasBin: true + levn@0.3.0: resolution: {integrity: sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==} engines: {node: '>= 0.8.0'} @@ -9223,10 +9586,17 @@ packages: resolution: {integrity: sha512-h6QFWd6lBMfjESqgQ27GjzrSDb0qbznp7VDQqp2zvgsrWut4vcchyMIzOVXvGQ2GMZgKw9RWrFNWv9WqGL0p7Q==} engines: {node: ^20.19.0 || >=22.12.0} + oxc-parser@0.143.0: + resolution: {integrity: sha512-ov0NzaDCOInknS7mP1cwKdJERt3utPW8ldjtdUXQ8Ty0GEFD08wk422vCUN0d7pST6kqtV7dxoI9w1Zi0l/9TA==} + engines: {node: ^20.19.0 || >=22.12.0} + oxc-parser@0.70.0: resolution: {integrity: sha512-YbqTuQDDIYwQF/li0VFK5uTbmHV4jWFeQQONkPdf77vz+JMiq7SusmcSVZ4hBrGM+3WyLdKH5S7spnvz4XVVzQ==} engines: {node: '>=14.0.0'} + oxc-resolver@11.24.2: + resolution: {integrity: sha512-FY91FiDBj7ls5MsFS9jN3tjz2o0/zsdSsymlakySaBwVJZorHhkWyICLZMKxlu1R9vYo+sd3z1jwb4J8x7bNDw==} + oxc-transform@0.141.0: resolution: {integrity: sha512-CrMPblXfPl57WIvGjynrUThcyEGcS3E3YUHkPYAwXYYla23gLlaIYifVFTMQsFr5uxA/2k/XMG5n6c/XsuAvaQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -9899,6 +10269,11 @@ packages: engines: {node: '>=14'} hasBin: true + prettier@3.9.6: + resolution: {integrity: sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==} + engines: {node: '>=14'} + hasBin: true + pretty-bytes@7.1.0: resolution: {integrity: sha512-nODzvTiYVRGRqAOvE84Vk5JDPyyxsVk0/fbA/bq7RqlnhksGpset09XTxbpvLTIjoaF7K8Z8DG8yHtKGTPSYRw==} engines: {node: '>=20'} @@ -11285,6 +11660,10 @@ packages: ultrahtml@1.7.0: resolution: {integrity: sha512-2xRd0VHoAQE4M+vF/DvFFB7pUV0ZxTW1TLi7lHQWnF/Sb5TPeEUV/l+hxcNnGO00ZXGnR0voCMmYRKQf+rvJ2g==} + unbash@4.0.10: + resolution: {integrity: sha512-b7zoBQvpWp0vuN5q2vK2RRBR2SvuruQAs50DApdDveBSn3eSYd84IaHodFqQIMlvY9K2VnyBUEXgwOBuGU9GBg==} + engines: {node: '>=14'} + unbox-primitive@1.1.0: resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} engines: {node: '>= 0.4'} @@ -11928,6 +12307,10 @@ packages: resolution: {integrity: sha512-4tzD0mF8iSiMiNs30BiLO3EpfGLZUT2MSX/G+o7ZywDzliWQ3OPtTZ0PTC3B3ca1UAf4cJMHB+2Bf56EriJuRA==} engines: {node: '>=10'} + walk-up-path@4.0.0: + resolution: {integrity: sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A==} + engines: {node: 20 || >=22} + web-namespaces@2.0.1: resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==} @@ -12215,6 +12598,39 @@ snapshots: '@ark/util@0.56.0': {} + '@ast-grep/cli-darwin-arm64@0.45.2': + optional: true + + '@ast-grep/cli-darwin-x64@0.45.2': + optional: true + + '@ast-grep/cli-linux-arm64-gnu@0.45.2': + optional: true + + '@ast-grep/cli-linux-x64-gnu@0.45.2': + optional: true + + '@ast-grep/cli-win32-arm64-msvc@0.45.2': + optional: true + + '@ast-grep/cli-win32-ia32-msvc@0.45.2': + optional: true + + '@ast-grep/cli-win32-x64-msvc@0.45.2': + optional: true + + '@ast-grep/cli@0.45.2': + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + '@ast-grep/cli-darwin-arm64': 0.45.2 + '@ast-grep/cli-darwin-x64': 0.45.2 + '@ast-grep/cli-linux-arm64-gnu': 0.45.2 + '@ast-grep/cli-linux-x64-gnu': 0.45.2 + '@ast-grep/cli-win32-arm64-msvc': 0.45.2 + '@ast-grep/cli-win32-ia32-msvc': 0.45.2 + '@ast-grep/cli-win32-x64-msvc': 0.45.2 + '@astrojs/compiler-binding-darwin-arm64@0.3.1': optional: true @@ -14065,7 +14481,14 @@ snapshots: '@tybys/wasm-util': 0.10.3 optional: true - '@next/env@15.5.21': {} + '@napi-rs/wasm-runtime@1.2.3(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.2)': + dependencies: + '@emnapi/core': 1.11.2 + '@emnapi/runtime': 1.11.2 + '@tybys/wasm-util': 0.10.3 + optional: true + + '@next/env@15.5.21': {} '@next/eslint-plugin-next@15.4.4': dependencies: @@ -14197,9 +14620,9 @@ snapshots: transitivePeerDependencies: - magicast - '@nuxt/devtools-kit@3.2.4(magic-string@1.1.0)(magicast@0.5.2)(oxc-parser@0.140.0)(rolldown@1.1.5)(unplugin@3.3.0(esbuild@0.28.0)(rolldown@1.1.5)(rollup@4.62.4)(vite@8.1.5(@types/node@22.19.17)(esbuild@0.28.0)(jiti@2.7.0)(sass@1.99.0)(terser@5.47.1)(yaml@2.9.0)))(vite@8.1.5(@types/node@22.19.17)(esbuild@0.28.0)(jiti@2.7.0)(sass@1.99.0)(terser@5.47.1)(yaml@2.9.0))': + '@nuxt/devtools-kit@3.2.4(magic-string@1.1.0)(magicast@0.5.2)(oxc-parser@0.143.0)(rolldown@1.1.5)(unplugin@3.3.0(esbuild@0.28.0)(rolldown@1.1.5)(rollup@4.62.4)(vite@8.1.5(@types/node@22.19.17)(esbuild@0.28.0)(jiti@2.7.0)(sass@1.99.0)(terser@5.47.1)(yaml@2.9.0)))(vite@8.1.5(@types/node@22.19.17)(esbuild@0.28.0)(jiti@2.7.0)(sass@1.99.0)(terser@5.47.1)(yaml@2.9.0))': dependencies: - '@nuxt/kit': 4.5.2(magic-string@1.1.0)(magicast@0.5.2)(oxc-parser@0.140.0)(rolldown@1.1.5)(unplugin@3.3.0(esbuild@0.28.0)(rolldown@1.1.5)(rollup@4.62.4)(vite@8.1.5(@types/node@22.19.17)(esbuild@0.28.0)(jiti@2.7.0)(sass@1.99.0)(terser@5.47.1)(yaml@2.9.0))) + '@nuxt/kit': 4.5.2(magic-string@1.1.0)(magicast@0.5.2)(oxc-parser@0.143.0)(rolldown@1.1.5)(unplugin@3.3.0(esbuild@0.28.0)(rolldown@1.1.5)(rollup@4.62.4)(vite@8.1.5(@types/node@22.19.17)(esbuild@0.28.0)(jiti@2.7.0)(sass@1.99.0)(terser@5.47.1)(yaml@2.9.0))) execa: 8.0.1 vite: 8.1.5(@types/node@22.19.17)(esbuild@0.28.0)(jiti@2.7.0)(sass@1.99.0)(terser@5.47.1)(yaml@2.9.0) transitivePeerDependencies: @@ -14454,10 +14877,10 @@ snapshots: - supports-color - typescript - '@nuxt/eslint@1.15.2(@typescript-eslint/utils@8.58.2(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(@vue/compiler-sfc@3.5.41)(eslint-import-resolver-node@0.3.10)(eslint@9.39.4(jiti@2.7.0))(magic-string@1.1.0)(magicast@0.5.2)(oxc-parser@0.140.0)(rolldown@1.1.5)(typescript@5.9.3)(unplugin@3.3.0(esbuild@0.28.0)(rolldown@1.1.5)(rollup@4.62.4)(vite@8.1.5(@types/node@22.19.17)(esbuild@0.28.0)(jiti@2.7.0)(sass@1.99.0)(terser@5.47.1)(yaml@2.9.0)))(vite@8.1.5(@types/node@22.19.17)(esbuild@0.28.0)(jiti@2.7.0)(sass@1.99.0)(terser@5.47.1)(yaml@2.9.0))': + '@nuxt/eslint@1.15.2(@typescript-eslint/utils@8.58.2(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(@vue/compiler-sfc@3.5.41)(eslint-import-resolver-node@0.3.10)(eslint@9.39.4(jiti@2.7.0))(magic-string@1.1.0)(magicast@0.5.2)(oxc-parser@0.143.0)(rolldown@1.1.5)(typescript@5.9.3)(unplugin@3.3.0(esbuild@0.28.0)(rolldown@1.1.5)(rollup@4.62.4)(vite@8.1.5(@types/node@22.19.17)(esbuild@0.28.0)(jiti@2.7.0)(sass@1.99.0)(terser@5.47.1)(yaml@2.9.0)))(vite@8.1.5(@types/node@22.19.17)(esbuild@0.28.0)(jiti@2.7.0)(sass@1.99.0)(terser@5.47.1)(yaml@2.9.0))': dependencies: '@eslint/config-inspector': 1.5.0(eslint@9.39.4(jiti@2.7.0)) - '@nuxt/devtools-kit': 3.2.4(magic-string@1.1.0)(magicast@0.5.2)(oxc-parser@0.140.0)(rolldown@1.1.5)(unplugin@3.3.0(esbuild@0.28.0)(rolldown@1.1.5)(rollup@4.62.4)(vite@8.1.5(@types/node@22.19.17)(esbuild@0.28.0)(jiti@2.7.0)(sass@1.99.0)(terser@5.47.1)(yaml@2.9.0)))(vite@8.1.5(@types/node@22.19.17)(esbuild@0.28.0)(jiti@2.7.0)(sass@1.99.0)(terser@5.47.1)(yaml@2.9.0)) + '@nuxt/devtools-kit': 3.2.4(magic-string@1.1.0)(magicast@0.5.2)(oxc-parser@0.143.0)(rolldown@1.1.5)(unplugin@3.3.0(esbuild@0.28.0)(rolldown@1.1.5)(rollup@4.62.4)(vite@8.1.5(@types/node@22.19.17)(esbuild@0.28.0)(jiti@2.7.0)(sass@1.99.0)(terser@5.47.1)(yaml@2.9.0)))(vite@8.1.5(@types/node@22.19.17)(esbuild@0.28.0)(jiti@2.7.0)(sass@1.99.0)(terser@5.47.1)(yaml@2.9.0)) '@nuxt/eslint-config': 1.15.2(@typescript-eslint/utils@8.58.2(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(@vue/compiler-sfc@3.5.41)(eslint-import-resolver-node@0.3.10)(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) '@nuxt/eslint-plugin': 1.15.2(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) '@nuxt/kit': 4.4.2(magicast@0.5.2) @@ -14834,6 +15257,36 @@ snapshots: - rolldown - unplugin + '@nuxt/kit@4.5.2(magic-string@1.1.0)(magicast@0.5.2)(oxc-parser@0.143.0)(rolldown@1.1.5)(unplugin@3.3.0(esbuild@0.28.0)(rolldown@1.1.5)(rollup@4.62.4)(vite@8.1.5(@types/node@22.19.17)(esbuild@0.28.0)(jiti@2.7.0)(sass@1.99.0)(terser@5.47.1)(yaml@2.9.0)))': + dependencies: + c12: 3.3.4(magicast@0.5.2) + consola: 3.4.2 + defu: 6.1.7 + destr: 2.0.5 + errx: 0.1.2 + exsolve: 1.1.1 + ignore: 7.0.6 + jiti: 2.7.0 + klona: 2.0.6 + mlly: 1.8.2 + nostics: 1.2.0 + ohash: 2.0.11 + pathe: 2.0.3 + pkg-types: 2.3.1 + rc9: 3.0.1 + scule: 1.3.0 + tinyglobby: 0.2.17 + ufo: 1.6.4 + unctx: 3.0.0(magic-string@1.1.0)(oxc-parser@0.143.0)(rolldown@1.1.5)(unplugin@3.3.0(esbuild@0.28.0)(rolldown@1.1.5)(rollup@4.62.4)(vite@8.1.5(@types/node@22.19.17)(esbuild@0.28.0)(jiti@2.7.0)(sass@1.99.0)(terser@5.47.1)(yaml@2.9.0))) + untyped: 2.0.0 + verkit: 0.3.2 + transitivePeerDependencies: + - magic-string + - magicast + - oxc-parser + - rolldown + - unplugin + '@nuxt/kit@4.5.2(magic-string@1.1.0)(magicast@0.5.4)(oxc-parser@0.140.0)(rolldown@1.1.5)(unplugin@3.3.0(esbuild@0.28.1)(rolldown@1.1.5)(rollup@4.62.4)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.99.0)(terser@5.47.1)(yaml@2.9.0)))': dependencies: c12: 3.3.4(magicast@0.5.4) @@ -14864,7 +15317,7 @@ snapshots: - rolldown - unplugin - '@nuxt/nitro-server@3.21.10(@electric-sql/pglite@0.4.1)(db0@0.3.4(@electric-sql/pglite@0.4.1)(mysql2@3.15.3))(esbuild@0.28.0)(ioredis@5.10.1)(magicast@0.5.2)(mysql2@3.15.3)(nuxt@3.21.10(@electric-sql/pglite@0.4.1)(@oxc-project/types@0.140.0)(@parcel/watcher@2.5.6)(@types/node@22.19.17)(@vue/compiler-sfc@3.5.41)(cac@6.7.14)(db0@0.3.4(@electric-sql/pglite@0.4.1)(mysql2@3.15.3))(esbuild@0.28.0)(eslint@9.39.4(jiti@2.7.0))(ioredis@5.10.1)(lightningcss@1.33.0)(magicast@0.5.2)(mysql2@3.15.3)(optionator@0.9.4)(rolldown@1.1.5)(rollup-plugin-visualizer@7.0.1(rolldown@1.1.5)(rollup@4.62.4))(rollup@4.62.4)(sass@1.99.0)(terser@5.47.1)(typescript@5.9.3)(vite@8.1.5(@types/node@22.19.17)(esbuild@0.28.0)(jiti@2.7.0)(sass@1.99.0)(terser@5.47.1)(yaml@2.9.0))(vue-tsc@3.2.6(typescript@5.9.3))(yaml@2.9.0))(oxc-parser@0.140.0)(rolldown@1.1.5)(rollup@4.62.4)(typescript@5.9.3)(vite@8.1.5(@types/node@22.19.17)(esbuild@0.28.0)(jiti@2.7.0)(sass@1.99.0)(terser@5.47.1)(yaml@2.9.0))': + '@nuxt/nitro-server@3.21.10(@electric-sql/pglite@0.4.1)(db0@0.3.4(@electric-sql/pglite@0.4.1)(mysql2@3.15.3))(esbuild@0.28.0)(ioredis@5.10.1)(magicast@0.5.2)(mysql2@3.15.3)(nuxt@3.21.10(@electric-sql/pglite@0.4.1)(@oxc-project/types@0.143.0)(@parcel/watcher@2.5.6)(@types/node@22.19.17)(@vue/compiler-sfc@3.5.41)(cac@6.7.14)(db0@0.3.4(@electric-sql/pglite@0.4.1)(mysql2@3.15.3))(esbuild@0.28.0)(eslint@9.39.4(jiti@2.7.0))(ioredis@5.10.1)(lightningcss@1.33.0)(magicast@0.5.2)(mysql2@3.15.3)(optionator@0.9.4)(rolldown@1.1.5)(rollup-plugin-visualizer@7.0.1(rolldown@1.1.5)(rollup@4.62.4))(rollup@4.62.4)(sass@1.99.0)(terser@5.47.1)(typescript@5.9.3)(vite@8.1.5(@types/node@22.19.17)(esbuild@0.28.0)(jiti@2.7.0)(sass@1.99.0)(terser@5.47.1)(yaml@2.9.0))(vue-tsc@3.2.6(typescript@5.9.3))(yaml@2.9.0))(oxc-parser@0.140.0)(rolldown@1.1.5)(rollup@4.62.4)(typescript@5.9.3)(vite@8.1.5(@types/node@22.19.17)(esbuild@0.28.0)(jiti@2.7.0)(sass@1.99.0)(terser@5.47.1)(yaml@2.9.0))': dependencies: '@nuxt/devalue': 2.0.2 '@nuxt/kit': 3.21.10(magicast@0.5.2) @@ -14882,7 +15335,7 @@ snapshots: klona: 2.0.6 mocked-exports: 0.1.1 nitropack: 2.13.4(@electric-sql/pglite@0.4.1)(mysql2@3.15.3)(oxc-parser@0.140.0)(rolldown@1.1.5)(vite@8.1.5(@types/node@22.19.17)(esbuild@0.28.0)(jiti@2.7.0)(sass@1.99.0)(terser@5.47.1)(yaml@2.9.0)) - nuxt: 3.21.10(@electric-sql/pglite@0.4.1)(@oxc-project/types@0.140.0)(@parcel/watcher@2.5.6)(@types/node@22.19.17)(@vue/compiler-sfc@3.5.41)(cac@6.7.14)(db0@0.3.4(@electric-sql/pglite@0.4.1)(mysql2@3.15.3))(esbuild@0.28.0)(eslint@9.39.4(jiti@2.7.0))(ioredis@5.10.1)(lightningcss@1.33.0)(magicast@0.5.2)(mysql2@3.15.3)(optionator@0.9.4)(rolldown@1.1.5)(rollup-plugin-visualizer@7.0.1(rolldown@1.1.5)(rollup@4.62.4))(rollup@4.62.4)(sass@1.99.0)(terser@5.47.1)(typescript@5.9.3)(vite@8.1.5(@types/node@22.19.17)(esbuild@0.28.0)(jiti@2.7.0)(sass@1.99.0)(terser@5.47.1)(yaml@2.9.0))(vue-tsc@3.2.6(typescript@5.9.3))(yaml@2.9.0) + nuxt: 3.21.10(@electric-sql/pglite@0.4.1)(@oxc-project/types@0.143.0)(@parcel/watcher@2.5.6)(@types/node@22.19.17)(@vue/compiler-sfc@3.5.41)(cac@6.7.14)(db0@0.3.4(@electric-sql/pglite@0.4.1)(mysql2@3.15.3))(esbuild@0.28.0)(eslint@9.39.4(jiti@2.7.0))(ioredis@5.10.1)(lightningcss@1.33.0)(magicast@0.5.2)(mysql2@3.15.3)(optionator@0.9.4)(rolldown@1.1.5)(rollup-plugin-visualizer@7.0.1(rolldown@1.1.5)(rollup@4.62.4))(rollup@4.62.4)(sass@1.99.0)(terser@5.47.1)(typescript@5.9.3)(vite@8.1.5(@types/node@22.19.17)(esbuild@0.28.0)(jiti@2.7.0)(sass@1.99.0)(terser@5.47.1)(yaml@2.9.0))(vue-tsc@3.2.6(typescript@5.9.3))(yaml@2.9.0) ohash: 2.0.11 pathe: 2.0.3 pkg-types: 2.3.1 @@ -14939,7 +15392,7 @@ snapshots: - webpack - xml2js - '@nuxt/nitro-server@3.21.10(@electric-sql/pglite@0.4.1)(db0@0.3.4(@electric-sql/pglite@0.4.1)(mysql2@3.15.3))(esbuild@0.28.1)(ioredis@5.10.1)(magicast@0.5.4)(mysql2@3.15.3)(nuxt@3.21.10(@electric-sql/pglite@0.4.1)(@oxc-project/types@0.140.0)(@parcel/watcher@2.5.6)(@types/node@24.13.3)(@vue/compiler-sfc@3.5.41)(cac@7.0.0)(db0@0.3.4(@electric-sql/pglite@0.4.1)(mysql2@3.15.3))(esbuild@0.28.1)(eslint@9.39.4(jiti@2.7.0))(ioredis@5.10.1)(lightningcss@1.33.0)(magicast@0.5.4)(mysql2@3.15.3)(optionator@0.9.4)(rolldown@1.1.5)(rollup-plugin-visualizer@7.0.1(rolldown@1.1.5)(rollup@4.62.4))(rollup@4.62.4)(sass@1.99.0)(terser@5.47.1)(typescript@5.9.3)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.99.0)(terser@5.47.1)(yaml@2.9.0))(vue-tsc@3.2.6(typescript@5.9.3))(yaml@2.9.0))(oxc-parser@0.140.0)(rolldown@1.1.5)(rollup@4.62.4)(typescript@5.9.3)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.99.0)(terser@5.47.1)(yaml@2.9.0))': + '@nuxt/nitro-server@3.21.10(@electric-sql/pglite@0.4.1)(db0@0.3.4(@electric-sql/pglite@0.4.1)(mysql2@3.15.3))(esbuild@0.28.1)(ioredis@5.10.1)(magicast@0.5.4)(mysql2@3.15.3)(nuxt@3.21.10(@electric-sql/pglite@0.4.1)(@oxc-project/types@0.143.0)(@parcel/watcher@2.5.6)(@types/node@24.13.3)(@vue/compiler-sfc@3.5.41)(cac@7.0.0)(db0@0.3.4(@electric-sql/pglite@0.4.1)(mysql2@3.15.3))(esbuild@0.28.1)(eslint@9.39.4(jiti@2.7.0))(ioredis@5.10.1)(lightningcss@1.33.0)(magicast@0.5.4)(mysql2@3.15.3)(optionator@0.9.4)(rolldown@1.1.5)(rollup-plugin-visualizer@7.0.1(rolldown@1.1.5)(rollup@4.62.4))(rollup@4.62.4)(sass@1.99.0)(terser@5.47.1)(typescript@5.9.3)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.99.0)(terser@5.47.1)(yaml@2.9.0))(vue-tsc@3.2.6(typescript@5.9.3))(yaml@2.9.0))(oxc-parser@0.140.0)(rolldown@1.1.5)(rollup@4.62.4)(typescript@5.9.3)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.99.0)(terser@5.47.1)(yaml@2.9.0))': dependencies: '@nuxt/devalue': 2.0.2 '@nuxt/kit': 3.21.10(magicast@0.5.4) @@ -14957,7 +15410,7 @@ snapshots: klona: 2.0.6 mocked-exports: 0.1.1 nitropack: 2.13.4(@electric-sql/pglite@0.4.1)(mysql2@3.15.3)(oxc-parser@0.140.0)(rolldown@1.1.5)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.99.0)(terser@5.47.1)(yaml@2.9.0)) - nuxt: 3.21.10(@electric-sql/pglite@0.4.1)(@oxc-project/types@0.140.0)(@parcel/watcher@2.5.6)(@types/node@24.13.3)(@vue/compiler-sfc@3.5.41)(cac@7.0.0)(db0@0.3.4(@electric-sql/pglite@0.4.1)(mysql2@3.15.3))(esbuild@0.28.1)(eslint@9.39.4(jiti@2.7.0))(ioredis@5.10.1)(lightningcss@1.33.0)(magicast@0.5.4)(mysql2@3.15.3)(optionator@0.9.4)(rolldown@1.1.5)(rollup-plugin-visualizer@7.0.1(rolldown@1.1.5)(rollup@4.62.4))(rollup@4.62.4)(sass@1.99.0)(terser@5.47.1)(typescript@5.9.3)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.99.0)(terser@5.47.1)(yaml@2.9.0))(vue-tsc@3.2.6(typescript@5.9.3))(yaml@2.9.0) + nuxt: 3.21.10(@electric-sql/pglite@0.4.1)(@oxc-project/types@0.143.0)(@parcel/watcher@2.5.6)(@types/node@24.13.3)(@vue/compiler-sfc@3.5.41)(cac@7.0.0)(db0@0.3.4(@electric-sql/pglite@0.4.1)(mysql2@3.15.3))(esbuild@0.28.1)(eslint@9.39.4(jiti@2.7.0))(ioredis@5.10.1)(lightningcss@1.33.0)(magicast@0.5.4)(mysql2@3.15.3)(optionator@0.9.4)(rolldown@1.1.5)(rollup-plugin-visualizer@7.0.1(rolldown@1.1.5)(rollup@4.62.4))(rollup@4.62.4)(sass@1.99.0)(terser@5.47.1)(typescript@5.9.3)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.99.0)(terser@5.47.1)(yaml@2.9.0))(vue-tsc@3.2.6(typescript@5.9.3))(yaml@2.9.0) ohash: 2.0.11 pathe: 2.0.3 pkg-types: 2.3.1 @@ -15040,7 +15493,7 @@ snapshots: rc9: 3.0.1 std-env: 4.2.0 - '@nuxt/vite-builder@3.21.10(@types/node@22.19.17)(eslint@9.39.4(jiti@2.7.0))(lightningcss@1.33.0)(magicast@0.5.2)(nuxt@3.21.10(@electric-sql/pglite@0.4.1)(@oxc-project/types@0.140.0)(@parcel/watcher@2.5.6)(@types/node@22.19.17)(@vue/compiler-sfc@3.5.41)(cac@6.7.14)(db0@0.3.4(@electric-sql/pglite@0.4.1)(mysql2@3.15.3))(esbuild@0.28.0)(eslint@9.39.4(jiti@2.7.0))(ioredis@5.10.1)(lightningcss@1.33.0)(magicast@0.5.2)(mysql2@3.15.3)(optionator@0.9.4)(rolldown@1.1.5)(rollup-plugin-visualizer@7.0.1(rolldown@1.1.5)(rollup@4.62.4))(rollup@4.62.4)(sass@1.99.0)(terser@5.47.1)(typescript@5.9.3)(vite@8.1.5(@types/node@22.19.17)(esbuild@0.28.0)(jiti@2.7.0)(sass@1.99.0)(terser@5.47.1)(yaml@2.9.0))(vue-tsc@3.2.6(typescript@5.9.3))(yaml@2.9.0))(optionator@0.9.4)(rolldown@1.1.5)(rollup-plugin-visualizer@7.0.1(rolldown@1.1.5)(rollup@4.62.4))(rollup@4.62.4)(sass@1.99.0)(terser@5.47.1)(typescript@5.9.3)(vue-tsc@3.2.6(typescript@5.9.3))(vue@3.5.41(typescript@5.9.3))(yaml@2.9.0)': + '@nuxt/vite-builder@3.21.10(@types/node@22.19.17)(eslint@9.39.4(jiti@2.7.0))(lightningcss@1.33.0)(magicast@0.5.2)(nuxt@3.21.10(@electric-sql/pglite@0.4.1)(@oxc-project/types@0.143.0)(@parcel/watcher@2.5.6)(@types/node@22.19.17)(@vue/compiler-sfc@3.5.41)(cac@6.7.14)(db0@0.3.4(@electric-sql/pglite@0.4.1)(mysql2@3.15.3))(esbuild@0.28.0)(eslint@9.39.4(jiti@2.7.0))(ioredis@5.10.1)(lightningcss@1.33.0)(magicast@0.5.2)(mysql2@3.15.3)(optionator@0.9.4)(rolldown@1.1.5)(rollup-plugin-visualizer@7.0.1(rolldown@1.1.5)(rollup@4.62.4))(rollup@4.62.4)(sass@1.99.0)(terser@5.47.1)(typescript@5.9.3)(vite@8.1.5(@types/node@22.19.17)(esbuild@0.28.0)(jiti@2.7.0)(sass@1.99.0)(terser@5.47.1)(yaml@2.9.0))(vue-tsc@3.2.6(typescript@5.9.3))(yaml@2.9.0))(optionator@0.9.4)(rolldown@1.1.5)(rollup-plugin-visualizer@7.0.1(rolldown@1.1.5)(rollup@4.62.4))(rollup@4.62.4)(sass@1.99.0)(terser@5.47.1)(typescript@5.9.3)(vue-tsc@3.2.6(typescript@5.9.3))(vue@3.5.41(typescript@5.9.3))(yaml@2.9.0)': dependencies: '@nuxt/kit': 3.21.10(magicast@0.5.2) '@rollup/plugin-replace': 6.0.3(rollup@4.62.4) @@ -15060,7 +15513,7 @@ snapshots: magic-string: 0.30.21 mlly: 1.8.2 mocked-exports: 0.1.1 - nuxt: 3.21.10(@electric-sql/pglite@0.4.1)(@oxc-project/types@0.140.0)(@parcel/watcher@2.5.6)(@types/node@22.19.17)(@vue/compiler-sfc@3.5.41)(cac@6.7.14)(db0@0.3.4(@electric-sql/pglite@0.4.1)(mysql2@3.15.3))(esbuild@0.28.0)(eslint@9.39.4(jiti@2.7.0))(ioredis@5.10.1)(lightningcss@1.33.0)(magicast@0.5.2)(mysql2@3.15.3)(optionator@0.9.4)(rolldown@1.1.5)(rollup-plugin-visualizer@7.0.1(rolldown@1.1.5)(rollup@4.62.4))(rollup@4.62.4)(sass@1.99.0)(terser@5.47.1)(typescript@5.9.3)(vite@8.1.5(@types/node@22.19.17)(esbuild@0.28.0)(jiti@2.7.0)(sass@1.99.0)(terser@5.47.1)(yaml@2.9.0))(vue-tsc@3.2.6(typescript@5.9.3))(yaml@2.9.0) + nuxt: 3.21.10(@electric-sql/pglite@0.4.1)(@oxc-project/types@0.143.0)(@parcel/watcher@2.5.6)(@types/node@22.19.17)(@vue/compiler-sfc@3.5.41)(cac@6.7.14)(db0@0.3.4(@electric-sql/pglite@0.4.1)(mysql2@3.15.3))(esbuild@0.28.0)(eslint@9.39.4(jiti@2.7.0))(ioredis@5.10.1)(lightningcss@1.33.0)(magicast@0.5.2)(mysql2@3.15.3)(optionator@0.9.4)(rolldown@1.1.5)(rollup-plugin-visualizer@7.0.1(rolldown@1.1.5)(rollup@4.62.4))(rollup@4.62.4)(sass@1.99.0)(terser@5.47.1)(typescript@5.9.3)(vite@8.1.5(@types/node@22.19.17)(esbuild@0.28.0)(jiti@2.7.0)(sass@1.99.0)(terser@5.47.1)(yaml@2.9.0))(vue-tsc@3.2.6(typescript@5.9.3))(yaml@2.9.0) nypm: 0.6.9 ohash: 2.0.11 pathe: 2.0.3 @@ -15102,7 +15555,7 @@ snapshots: - vue-tsc - yaml - '@nuxt/vite-builder@3.21.10(@types/node@24.13.3)(eslint@9.39.4(jiti@2.7.0))(lightningcss@1.33.0)(magicast@0.5.4)(nuxt@3.21.10(@electric-sql/pglite@0.4.1)(@oxc-project/types@0.140.0)(@parcel/watcher@2.5.6)(@types/node@24.13.3)(@vue/compiler-sfc@3.5.41)(cac@7.0.0)(db0@0.3.4(@electric-sql/pglite@0.4.1)(mysql2@3.15.3))(esbuild@0.28.1)(eslint@9.39.4(jiti@2.7.0))(ioredis@5.10.1)(lightningcss@1.33.0)(magicast@0.5.4)(mysql2@3.15.3)(optionator@0.9.4)(rolldown@1.1.5)(rollup-plugin-visualizer@7.0.1(rolldown@1.1.5)(rollup@4.62.4))(rollup@4.62.4)(sass@1.99.0)(terser@5.47.1)(typescript@5.9.3)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.99.0)(terser@5.47.1)(yaml@2.9.0))(vue-tsc@3.2.6(typescript@5.9.3))(yaml@2.9.0))(optionator@0.9.4)(rolldown@1.1.5)(rollup-plugin-visualizer@7.0.1(rolldown@1.1.5)(rollup@4.62.4))(rollup@4.62.4)(sass@1.99.0)(terser@5.47.1)(typescript@5.9.3)(vue-tsc@3.2.6(typescript@5.9.3))(vue@3.5.41(typescript@5.9.3))(yaml@2.9.0)': + '@nuxt/vite-builder@3.21.10(@types/node@24.13.3)(eslint@9.39.4(jiti@2.7.0))(lightningcss@1.33.0)(magicast@0.5.4)(nuxt@3.21.10(@electric-sql/pglite@0.4.1)(@oxc-project/types@0.143.0)(@parcel/watcher@2.5.6)(@types/node@24.13.3)(@vue/compiler-sfc@3.5.41)(cac@7.0.0)(db0@0.3.4(@electric-sql/pglite@0.4.1)(mysql2@3.15.3))(esbuild@0.28.1)(eslint@9.39.4(jiti@2.7.0))(ioredis@5.10.1)(lightningcss@1.33.0)(magicast@0.5.4)(mysql2@3.15.3)(optionator@0.9.4)(rolldown@1.1.5)(rollup-plugin-visualizer@7.0.1(rolldown@1.1.5)(rollup@4.62.4))(rollup@4.62.4)(sass@1.99.0)(terser@5.47.1)(typescript@5.9.3)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.99.0)(terser@5.47.1)(yaml@2.9.0))(vue-tsc@3.2.6(typescript@5.9.3))(yaml@2.9.0))(optionator@0.9.4)(rolldown@1.1.5)(rollup-plugin-visualizer@7.0.1(rolldown@1.1.5)(rollup@4.62.4))(rollup@4.62.4)(sass@1.99.0)(terser@5.47.1)(typescript@5.9.3)(vue-tsc@3.2.6(typescript@5.9.3))(vue@3.5.41(typescript@5.9.3))(yaml@2.9.0)': dependencies: '@nuxt/kit': 3.21.10(magicast@0.5.4) '@rollup/plugin-replace': 6.0.3(rollup@4.62.4) @@ -15122,7 +15575,7 @@ snapshots: magic-string: 0.30.21 mlly: 1.8.2 mocked-exports: 0.1.1 - nuxt: 3.21.10(@electric-sql/pglite@0.4.1)(@oxc-project/types@0.140.0)(@parcel/watcher@2.5.6)(@types/node@24.13.3)(@vue/compiler-sfc@3.5.41)(cac@7.0.0)(db0@0.3.4(@electric-sql/pglite@0.4.1)(mysql2@3.15.3))(esbuild@0.28.1)(eslint@9.39.4(jiti@2.7.0))(ioredis@5.10.1)(lightningcss@1.33.0)(magicast@0.5.4)(mysql2@3.15.3)(optionator@0.9.4)(rolldown@1.1.5)(rollup-plugin-visualizer@7.0.1(rolldown@1.1.5)(rollup@4.62.4))(rollup@4.62.4)(sass@1.99.0)(terser@5.47.1)(typescript@5.9.3)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.99.0)(terser@5.47.1)(yaml@2.9.0))(vue-tsc@3.2.6(typescript@5.9.3))(yaml@2.9.0) + nuxt: 3.21.10(@electric-sql/pglite@0.4.1)(@oxc-project/types@0.143.0)(@parcel/watcher@2.5.6)(@types/node@24.13.3)(@vue/compiler-sfc@3.5.41)(cac@7.0.0)(db0@0.3.4(@electric-sql/pglite@0.4.1)(mysql2@3.15.3))(esbuild@0.28.1)(eslint@9.39.4(jiti@2.7.0))(ioredis@5.10.1)(lightningcss@1.33.0)(magicast@0.5.4)(mysql2@3.15.3)(optionator@0.9.4)(rolldown@1.1.5)(rollup-plugin-visualizer@7.0.1(rolldown@1.1.5)(rollup@4.62.4))(rollup@4.62.4)(sass@1.99.0)(terser@5.47.1)(typescript@5.9.3)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.99.0)(terser@5.47.1)(yaml@2.9.0))(vue-tsc@3.2.6(typescript@5.9.3))(yaml@2.9.0) nypm: 0.6.9 ohash: 2.0.11 pathe: 2.0.3 @@ -15311,84 +15764,132 @@ snapshots: '@oxc-parser/binding-android-arm-eabi@0.140.0': optional: true + '@oxc-parser/binding-android-arm-eabi@0.143.0': + optional: true + '@oxc-parser/binding-android-arm64@0.140.0': optional: true + '@oxc-parser/binding-android-arm64@0.143.0': + optional: true + '@oxc-parser/binding-darwin-arm64@0.140.0': optional: true + '@oxc-parser/binding-darwin-arm64@0.143.0': + optional: true + '@oxc-parser/binding-darwin-arm64@0.70.0': optional: true '@oxc-parser/binding-darwin-x64@0.140.0': optional: true + '@oxc-parser/binding-darwin-x64@0.143.0': + optional: true + '@oxc-parser/binding-darwin-x64@0.70.0': optional: true '@oxc-parser/binding-freebsd-x64@0.140.0': optional: true + '@oxc-parser/binding-freebsd-x64@0.143.0': + optional: true + '@oxc-parser/binding-freebsd-x64@0.70.0': optional: true '@oxc-parser/binding-linux-arm-gnueabihf@0.140.0': optional: true + '@oxc-parser/binding-linux-arm-gnueabihf@0.143.0': + optional: true + '@oxc-parser/binding-linux-arm-gnueabihf@0.70.0': optional: true '@oxc-parser/binding-linux-arm-musleabihf@0.140.0': optional: true + '@oxc-parser/binding-linux-arm-musleabihf@0.143.0': + optional: true + '@oxc-parser/binding-linux-arm-musleabihf@0.70.0': optional: true '@oxc-parser/binding-linux-arm64-gnu@0.140.0': optional: true + '@oxc-parser/binding-linux-arm64-gnu@0.143.0': + optional: true + '@oxc-parser/binding-linux-arm64-gnu@0.70.0': optional: true '@oxc-parser/binding-linux-arm64-musl@0.140.0': optional: true + '@oxc-parser/binding-linux-arm64-musl@0.143.0': + optional: true + '@oxc-parser/binding-linux-arm64-musl@0.70.0': optional: true '@oxc-parser/binding-linux-ppc64-gnu@0.140.0': optional: true + '@oxc-parser/binding-linux-ppc64-gnu@0.143.0': + optional: true + '@oxc-parser/binding-linux-riscv64-gnu@0.140.0': optional: true + '@oxc-parser/binding-linux-riscv64-gnu@0.143.0': + optional: true + '@oxc-parser/binding-linux-riscv64-gnu@0.70.0': optional: true '@oxc-parser/binding-linux-riscv64-musl@0.140.0': optional: true + '@oxc-parser/binding-linux-riscv64-musl@0.143.0': + optional: true + '@oxc-parser/binding-linux-s390x-gnu@0.140.0': optional: true + '@oxc-parser/binding-linux-s390x-gnu@0.143.0': + optional: true + '@oxc-parser/binding-linux-s390x-gnu@0.70.0': optional: true '@oxc-parser/binding-linux-x64-gnu@0.140.0': optional: true + '@oxc-parser/binding-linux-x64-gnu@0.143.0': + optional: true + '@oxc-parser/binding-linux-x64-gnu@0.70.0': optional: true '@oxc-parser/binding-linux-x64-musl@0.140.0': optional: true + '@oxc-parser/binding-linux-x64-musl@0.143.0': + optional: true + '@oxc-parser/binding-linux-x64-musl@0.70.0': optional: true '@oxc-parser/binding-openharmony-arm64@0.140.0': optional: true + '@oxc-parser/binding-openharmony-arm64@0.143.0': + optional: true + '@oxc-parser/binding-wasm32-wasi@0.140.0': dependencies: '@emnapi/core': 1.11.2 @@ -15404,15 +15905,24 @@ snapshots: '@oxc-parser/binding-win32-arm64-msvc@0.140.0': optional: true + '@oxc-parser/binding-win32-arm64-msvc@0.143.0': + optional: true + '@oxc-parser/binding-win32-arm64-msvc@0.70.0': optional: true '@oxc-parser/binding-win32-ia32-msvc@0.140.0': optional: true + '@oxc-parser/binding-win32-ia32-msvc@0.143.0': + optional: true + '@oxc-parser/binding-win32-x64-msvc@0.140.0': optional: true + '@oxc-parser/binding-win32-x64-msvc@0.143.0': + optional: true + '@oxc-parser/binding-win32-x64-msvc@0.70.0': optional: true @@ -15424,10 +15934,73 @@ snapshots: '@oxc-project/types@0.140.0': {} + '@oxc-project/types@0.143.0': {} + '@oxc-project/types@0.60.0': {} '@oxc-project/types@0.70.0': {} + '@oxc-resolver/binding-android-arm-eabi@11.24.2': + optional: true + + '@oxc-resolver/binding-android-arm64@11.24.2': + optional: true + + '@oxc-resolver/binding-darwin-arm64@11.24.2': + optional: true + + '@oxc-resolver/binding-darwin-x64@11.24.2': + optional: true + + '@oxc-resolver/binding-freebsd-x64@11.24.2': + optional: true + + '@oxc-resolver/binding-linux-arm-gnueabihf@11.24.2': + optional: true + + '@oxc-resolver/binding-linux-arm-musleabihf@11.24.2': + optional: true + + '@oxc-resolver/binding-linux-arm64-gnu@11.24.2': + optional: true + + '@oxc-resolver/binding-linux-arm64-musl@11.24.2': + optional: true + + '@oxc-resolver/binding-linux-ppc64-gnu@11.24.2': + optional: true + + '@oxc-resolver/binding-linux-riscv64-gnu@11.24.2': + optional: true + + '@oxc-resolver/binding-linux-riscv64-musl@11.24.2': + optional: true + + '@oxc-resolver/binding-linux-s390x-gnu@11.24.2': + optional: true + + '@oxc-resolver/binding-linux-x64-gnu@11.24.2': + optional: true + + '@oxc-resolver/binding-linux-x64-musl@11.24.2': + optional: true + + '@oxc-resolver/binding-openharmony-arm64@11.24.2': + optional: true + + '@oxc-resolver/binding-wasm32-wasi@11.24.2': + dependencies: + '@emnapi/core': 1.11.2 + '@emnapi/runtime': 1.11.2 + '@napi-rs/wasm-runtime': 1.2.3(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.2) + optional: true + + '@oxc-resolver/binding-win32-arm64-msvc@11.24.2': + optional: true + + '@oxc-resolver/binding-win32-x64-msvc@11.24.2': + optional: true + '@oxc-transform/binding-android-arm-eabi@0.141.0': optional: true @@ -17203,13 +17776,13 @@ snapshots: '@vueuse/metadata@13.6.0': {} - '@vueuse/nuxt@13.6.0(magicast@0.5.2)(nuxt@3.21.10(@electric-sql/pglite@0.4.1)(@oxc-project/types@0.140.0)(@parcel/watcher@2.5.6)(@types/node@22.19.17)(@vue/compiler-sfc@3.5.41)(cac@6.7.14)(db0@0.3.4(@electric-sql/pglite@0.4.1)(mysql2@3.15.3))(esbuild@0.28.0)(eslint@9.39.4(jiti@2.7.0))(ioredis@5.10.1)(lightningcss@1.33.0)(magicast@0.5.2)(mysql2@3.15.3)(optionator@0.9.4)(rolldown@1.1.5)(rollup-plugin-visualizer@7.0.1(rolldown@1.1.5)(rollup@4.62.4))(rollup@4.62.4)(sass@1.99.0)(terser@5.47.1)(typescript@5.9.3)(vite@8.1.5(@types/node@22.19.17)(esbuild@0.28.0)(jiti@2.7.0)(sass@1.99.0)(terser@5.47.1)(yaml@2.9.0))(vue-tsc@3.2.6(typescript@5.9.3))(yaml@2.9.0))(vue@3.5.41(typescript@5.9.3))': + '@vueuse/nuxt@13.6.0(magicast@0.5.2)(nuxt@3.21.10(@electric-sql/pglite@0.4.1)(@oxc-project/types@0.143.0)(@parcel/watcher@2.5.6)(@types/node@22.19.17)(@vue/compiler-sfc@3.5.41)(cac@6.7.14)(db0@0.3.4(@electric-sql/pglite@0.4.1)(mysql2@3.15.3))(esbuild@0.28.0)(eslint@9.39.4(jiti@2.7.0))(ioredis@5.10.1)(lightningcss@1.33.0)(magicast@0.5.2)(mysql2@3.15.3)(optionator@0.9.4)(rolldown@1.1.5)(rollup-plugin-visualizer@7.0.1(rolldown@1.1.5)(rollup@4.62.4))(rollup@4.62.4)(sass@1.99.0)(terser@5.47.1)(typescript@5.9.3)(vite@8.1.5(@types/node@22.19.17)(esbuild@0.28.0)(jiti@2.7.0)(sass@1.99.0)(terser@5.47.1)(yaml@2.9.0))(vue-tsc@3.2.6(typescript@5.9.3))(yaml@2.9.0))(vue@3.5.41(typescript@5.9.3))': dependencies: '@nuxt/kit': 4.4.2(magicast@0.5.2) '@vueuse/core': 13.6.0(vue@3.5.41(typescript@5.9.3)) '@vueuse/metadata': 13.6.0 local-pkg: 1.1.2 - nuxt: 3.21.10(@electric-sql/pglite@0.4.1)(@oxc-project/types@0.140.0)(@parcel/watcher@2.5.6)(@types/node@22.19.17)(@vue/compiler-sfc@3.5.41)(cac@6.7.14)(db0@0.3.4(@electric-sql/pglite@0.4.1)(mysql2@3.15.3))(esbuild@0.28.0)(eslint@9.39.4(jiti@2.7.0))(ioredis@5.10.1)(lightningcss@1.33.0)(magicast@0.5.2)(mysql2@3.15.3)(optionator@0.9.4)(rolldown@1.1.5)(rollup-plugin-visualizer@7.0.1(rolldown@1.1.5)(rollup@4.62.4))(rollup@4.62.4)(sass@1.99.0)(terser@5.47.1)(typescript@5.9.3)(vite@8.1.5(@types/node@22.19.17)(esbuild@0.28.0)(jiti@2.7.0)(sass@1.99.0)(terser@5.47.1)(yaml@2.9.0))(vue-tsc@3.2.6(typescript@5.9.3))(yaml@2.9.0) + nuxt: 3.21.10(@electric-sql/pglite@0.4.1)(@oxc-project/types@0.143.0)(@parcel/watcher@2.5.6)(@types/node@22.19.17)(@vue/compiler-sfc@3.5.41)(cac@6.7.14)(db0@0.3.4(@electric-sql/pglite@0.4.1)(mysql2@3.15.3))(esbuild@0.28.0)(eslint@9.39.4(jiti@2.7.0))(ioredis@5.10.1)(lightningcss@1.33.0)(magicast@0.5.2)(mysql2@3.15.3)(optionator@0.9.4)(rolldown@1.1.5)(rollup-plugin-visualizer@7.0.1(rolldown@1.1.5)(rollup@4.62.4))(rollup@4.62.4)(sass@1.99.0)(terser@5.47.1)(typescript@5.9.3)(vite@8.1.5(@types/node@22.19.17)(esbuild@0.28.0)(jiti@2.7.0)(sass@1.99.0)(terser@5.47.1)(yaml@2.9.0))(vue-tsc@3.2.6(typescript@5.9.3))(yaml@2.9.0) vue: 3.5.41(typescript@5.9.3) transitivePeerDependencies: - magicast @@ -19092,7 +19665,7 @@ snapshots: '@typescript-eslint/parser': 8.58.2(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) eslint: 9.39.4(jiti@2.7.0) eslint-import-resolver-node: 0.3.10 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import-x@4.17.1(@typescript-eslint/utils@8.66.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint@9.39.4(jiti@2.7.0)))(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.58.2(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import-x@4.17.1(@typescript-eslint/utils@8.66.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint@9.39.4(jiti@2.7.0)))(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.7.0)) eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.58.2(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.7.0)) eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.4(jiti@2.7.0)) eslint-plugin-react: 7.37.5(eslint@9.39.4(jiti@2.7.0)) @@ -19140,7 +19713,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.10.1(eslint-plugin-import-x@4.17.1(@typescript-eslint/utils@8.66.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint@9.39.4(jiti@2.7.0)))(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.58.2(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)): + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import-x@4.17.1(@typescript-eslint/utils@8.66.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint@9.39.4(jiti@2.7.0)))(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.7.0)): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.3 @@ -19167,7 +19740,7 @@ snapshots: '@typescript-eslint/parser': 8.58.2(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) eslint: 9.39.4(jiti@2.7.0) eslint-import-resolver-node: 0.3.10 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import-x@4.17.1(@typescript-eslint/utils@8.66.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint@9.39.4(jiti@2.7.0)))(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.58.2(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import-x@4.17.1(@typescript-eslint/utils@8.66.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint@9.39.4(jiti@2.7.0)))(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.7.0)) transitivePeerDependencies: - supports-color @@ -19756,6 +20329,10 @@ snapshots: dependencies: format: 0.2.2 + fd-package-json@2.0.0: + dependencies: + walk-up-path: 4.0.0 + fd-slicer@1.1.0: dependencies: pend: 1.2.0 @@ -19901,6 +20478,10 @@ snapshots: format@0.2.2: {} + formatly@0.3.0: + dependencies: + fd-package-json: 2.0.0 + fraction.js@5.3.4: {} framer-motion@12.38.0(@emotion/is-prop-valid@1.4.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5): @@ -20028,6 +20609,10 @@ snapshots: dependencies: resolve-pkg-maps: 1.0.0 + get-tsconfig@4.14.1: + dependencies: + resolve-pkg-maps: 1.0.0 + get-tsconfig@5.0.0-beta.4: dependencies: resolve-pkg-maps: 1.0.0 @@ -21202,6 +21787,22 @@ snapshots: klona@2.0.6: {} + knip@6.32.2: + dependencies: + fdir: 6.5.0(picomatch@4.0.5) + formatly: 0.3.0 + get-tsconfig: 4.14.1 + jiti: 2.7.0 + oxc-parser: 0.143.0 + oxc-resolver: 11.24.2 + picomatch: 4.0.5 + smol-toml: 1.8.0 + strip-json-comments: 5.0.3 + tinyglobby: 0.2.17 + unbash: 4.0.10 + yaml: 2.9.0 + zod: 4.4.3 + knitwork@1.3.0: {} language-subtag-registry@0.3.23: {} @@ -21223,6 +21824,49 @@ snapshots: dependencies: readable-stream: 2.3.8 + lefthook-darwin-arm64@2.1.10: + optional: true + + lefthook-darwin-x64@2.1.10: + optional: true + + lefthook-freebsd-arm64@2.1.10: + optional: true + + lefthook-freebsd-x64@2.1.10: + optional: true + + lefthook-linux-arm64@2.1.10: + optional: true + + lefthook-linux-x64@2.1.10: + optional: true + + lefthook-openbsd-arm64@2.1.10: + optional: true + + lefthook-openbsd-x64@2.1.10: + optional: true + + lefthook-windows-arm64@2.1.10: + optional: true + + lefthook-windows-x64@2.1.10: + optional: true + + lefthook@2.1.10: + optionalDependencies: + lefthook-darwin-arm64: 2.1.10 + lefthook-darwin-x64: 2.1.10 + lefthook-freebsd-arm64: 2.1.10 + lefthook-freebsd-x64: 2.1.10 + lefthook-linux-arm64: 2.1.10 + lefthook-linux-x64: 2.1.10 + lefthook-openbsd-arm64: 2.1.10 + lefthook-openbsd-x64: 2.1.10 + lefthook-windows-arm64: 2.1.10 + lefthook-windows-x64: 2.1.10 + levn@0.3.0: dependencies: prelude-ls: 1.1.2 @@ -22441,6 +23085,116 @@ snapshots: - vite - webpack + nitropack@2.13.4(@electric-sql/pglite@0.4.1)(mysql2@3.15.3)(oxc-parser@0.143.0)(rolldown@1.1.5)(vite@8.1.5(@types/node@22.19.17)(esbuild@0.28.0)(jiti@2.7.0)(sass@1.99.0)(terser@5.47.1)(yaml@2.9.0)): + dependencies: + '@cloudflare/kv-asset-handler': 0.4.2 + '@rollup/plugin-alias': 6.0.0(rollup@4.60.3) + '@rollup/plugin-commonjs': 29.0.2(rollup@4.60.3) + '@rollup/plugin-inject': 5.0.5(rollup@4.60.3) + '@rollup/plugin-json': 6.1.0(rollup@4.60.3) + '@rollup/plugin-node-resolve': 16.0.3(rollup@4.60.3) + '@rollup/plugin-replace': 6.0.3(rollup@4.60.3) + '@rollup/plugin-terser': 1.0.0(rollup@4.60.3) + '@vercel/nft': 1.5.0(rollup@4.60.3) + archiver: 7.0.1 + c12: 3.3.4(magicast@0.5.2) + chokidar: 5.0.0 + citty: 0.2.2 + compatx: 0.2.0 + confbox: 0.2.4 + consola: 3.4.2 + cookie-es: 2.0.1 + croner: 10.0.1 + crossws: 0.3.5 + db0: 0.3.4(@electric-sql/pglite@0.4.1)(mysql2@3.15.3) + defu: 6.1.7 + destr: 2.0.5 + dot-prop: 10.1.0 + esbuild: 0.28.0 + escape-string-regexp: 5.0.0 + etag: 1.8.1 + exsolve: 1.0.8 + globby: 16.2.0 + gzip-size: 7.0.0 + h3: 1.15.11 + hookable: 5.5.3 + httpxy: 0.5.1 + ioredis: 5.10.1 + jiti: 2.7.0 + klona: 2.0.6 + knitwork: 1.3.0 + listhen: 1.10.0 + magic-string: 0.30.21 + magicast: 0.5.2 + mime: 4.1.0 + mlly: 1.8.2 + node-fetch-native: 1.6.7 + node-mock-http: 1.0.4 + ofetch: 1.5.1 + ohash: 2.0.11 + pathe: 2.0.3 + perfect-debounce: 2.1.0 + pkg-types: 2.3.1 + pretty-bytes: 7.1.0 + radix3: 1.1.2 + rollup: 4.60.3 + rollup-plugin-visualizer: 7.0.1(rolldown@1.1.5)(rollup@4.60.3) + scule: 1.3.0 + semver: 7.7.4 + serve-placeholder: 2.0.2 + serve-static: 2.2.1 + source-map: 0.7.6 + std-env: 4.1.0 + ufo: 1.6.4 + ultrahtml: 1.6.0 + uncrypto: 0.1.3 + unctx: 2.5.0 + unenv: 2.0.0-rc.24 + unimport: 6.2.0(esbuild@0.28.0)(oxc-parser@0.143.0)(rolldown@1.1.5)(rollup@4.60.3)(vite@8.1.5(@types/node@22.19.17)(esbuild@0.28.0)(jiti@2.7.0)(sass@1.99.0)(terser@5.47.1)(yaml@2.9.0)) + unplugin-utils: 0.3.1 + unstorage: 1.17.5(db0@0.3.4(@electric-sql/pglite@0.4.1)(mysql2@3.15.3))(ioredis@5.10.1) + untyped: 2.0.0 + unwasm: 0.5.3 + youch: 4.1.1 + youch-core: 0.3.3 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@electric-sql/pglite' + - '@farmfe/core' + - '@libsql/client' + - '@netlify/blobs' + - '@planetscale/database' + - '@rspack/core' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - aws4fetch + - bare-abort-controller + - bare-buffer + - better-sqlite3 + - bun-types-no-globals + - drizzle-orm + - encoding + - idb-keyval + - mysql2 + - oxc-parser + - react-native-b4a + - rolldown + - sqlite3 + - supports-color + - unloader + - uploadthing + - vite + - webpack + nlcst-to-string@4.0.0: dependencies: '@types/nlcst': 2.0.3 @@ -22551,16 +23305,16 @@ snapshots: - rollup - supports-color - nuxt@3.21.10(@electric-sql/pglite@0.4.1)(@oxc-project/types@0.140.0)(@parcel/watcher@2.5.6)(@types/node@22.19.17)(@vue/compiler-sfc@3.5.41)(cac@6.7.14)(db0@0.3.4(@electric-sql/pglite@0.4.1)(mysql2@3.15.3))(esbuild@0.28.0)(eslint@9.39.4(jiti@2.7.0))(ioredis@5.10.1)(lightningcss@1.33.0)(magicast@0.5.2)(mysql2@3.15.3)(optionator@0.9.4)(rolldown@1.1.5)(rollup-plugin-visualizer@7.0.1(rolldown@1.1.5)(rollup@4.62.4))(rollup@4.62.4)(sass@1.99.0)(terser@5.47.1)(typescript@5.9.3)(vite@8.1.5(@types/node@22.19.17)(esbuild@0.28.0)(jiti@2.7.0)(sass@1.99.0)(terser@5.47.1)(yaml@2.9.0))(vue-tsc@3.2.6(typescript@5.9.3))(yaml@2.9.0): + nuxt@3.21.10(@electric-sql/pglite@0.4.1)(@oxc-project/types@0.143.0)(@parcel/watcher@2.5.6)(@types/node@22.19.17)(@vue/compiler-sfc@3.5.41)(cac@6.7.14)(db0@0.3.4(@electric-sql/pglite@0.4.1)(mysql2@3.15.3))(esbuild@0.28.0)(eslint@9.39.4(jiti@2.7.0))(ioredis@5.10.1)(lightningcss@1.33.0)(magicast@0.5.2)(mysql2@3.15.3)(optionator@0.9.4)(rolldown@1.1.5)(rollup-plugin-visualizer@7.0.1(rolldown@1.1.5)(rollup@4.62.4))(rollup@4.62.4)(sass@1.99.0)(terser@5.47.1)(typescript@5.9.3)(vite@8.1.5(@types/node@22.19.17)(esbuild@0.28.0)(jiti@2.7.0)(sass@1.99.0)(terser@5.47.1)(yaml@2.9.0))(vue-tsc@3.2.6(typescript@5.9.3))(yaml@2.9.0): dependencies: '@dxup/nuxt': 0.5.6(esbuild@0.28.0)(magicast@0.5.2)(oxc-parser@0.140.0)(rolldown@1.1.5)(rollup@4.62.4)(vite@8.1.5(@types/node@22.19.17)(esbuild@0.28.0)(jiti@2.7.0)(sass@1.99.0)(terser@5.47.1)(yaml@2.9.0)) '@nuxt/cli': 3.37.0(@nuxt/schema@3.21.10)(@parcel/watcher@2.5.6)(cac@6.7.14)(magicast@0.5.2) '@nuxt/devtools': 3.4.1(db0@0.3.4(@electric-sql/pglite@0.4.1)(mysql2@3.15.3))(ioredis@5.10.1)(magic-string@0.30.21)(oxc-parser@0.140.0)(rolldown@1.1.5)(unplugin@3.3.0(esbuild@0.28.0)(rolldown@1.1.5)(rollup@4.62.4)(vite@8.1.5(@types/node@22.19.17)(esbuild@0.28.0)(jiti@2.7.0)(sass@1.99.0)(terser@5.47.1)(yaml@2.9.0)))(vite@8.1.5(@types/node@22.19.17)(esbuild@0.28.0)(jiti@2.7.0)(sass@1.99.0)(terser@5.47.1)(yaml@2.9.0))(vue@3.5.41(typescript@5.9.3)) '@nuxt/kit': 3.21.10(magicast@0.5.2) - '@nuxt/nitro-server': 3.21.10(@electric-sql/pglite@0.4.1)(db0@0.3.4(@electric-sql/pglite@0.4.1)(mysql2@3.15.3))(esbuild@0.28.0)(ioredis@5.10.1)(magicast@0.5.2)(mysql2@3.15.3)(nuxt@3.21.10(@electric-sql/pglite@0.4.1)(@oxc-project/types@0.140.0)(@parcel/watcher@2.5.6)(@types/node@22.19.17)(@vue/compiler-sfc@3.5.41)(cac@6.7.14)(db0@0.3.4(@electric-sql/pglite@0.4.1)(mysql2@3.15.3))(esbuild@0.28.0)(eslint@9.39.4(jiti@2.7.0))(ioredis@5.10.1)(lightningcss@1.33.0)(magicast@0.5.2)(mysql2@3.15.3)(optionator@0.9.4)(rolldown@1.1.5)(rollup-plugin-visualizer@7.0.1(rolldown@1.1.5)(rollup@4.62.4))(rollup@4.62.4)(sass@1.99.0)(terser@5.47.1)(typescript@5.9.3)(vite@8.1.5(@types/node@22.19.17)(esbuild@0.28.0)(jiti@2.7.0)(sass@1.99.0)(terser@5.47.1)(yaml@2.9.0))(vue-tsc@3.2.6(typescript@5.9.3))(yaml@2.9.0))(oxc-parser@0.140.0)(rolldown@1.1.5)(rollup@4.62.4)(typescript@5.9.3)(vite@8.1.5(@types/node@22.19.17)(esbuild@0.28.0)(jiti@2.7.0)(sass@1.99.0)(terser@5.47.1)(yaml@2.9.0)) + '@nuxt/nitro-server': 3.21.10(@electric-sql/pglite@0.4.1)(db0@0.3.4(@electric-sql/pglite@0.4.1)(mysql2@3.15.3))(esbuild@0.28.0)(ioredis@5.10.1)(magicast@0.5.2)(mysql2@3.15.3)(nuxt@3.21.10(@electric-sql/pglite@0.4.1)(@oxc-project/types@0.143.0)(@parcel/watcher@2.5.6)(@types/node@22.19.17)(@vue/compiler-sfc@3.5.41)(cac@6.7.14)(db0@0.3.4(@electric-sql/pglite@0.4.1)(mysql2@3.15.3))(esbuild@0.28.0)(eslint@9.39.4(jiti@2.7.0))(ioredis@5.10.1)(lightningcss@1.33.0)(magicast@0.5.2)(mysql2@3.15.3)(optionator@0.9.4)(rolldown@1.1.5)(rollup-plugin-visualizer@7.0.1(rolldown@1.1.5)(rollup@4.62.4))(rollup@4.62.4)(sass@1.99.0)(terser@5.47.1)(typescript@5.9.3)(vite@8.1.5(@types/node@22.19.17)(esbuild@0.28.0)(jiti@2.7.0)(sass@1.99.0)(terser@5.47.1)(yaml@2.9.0))(vue-tsc@3.2.6(typescript@5.9.3))(yaml@2.9.0))(oxc-parser@0.140.0)(rolldown@1.1.5)(rollup@4.62.4)(typescript@5.9.3)(vite@8.1.5(@types/node@22.19.17)(esbuild@0.28.0)(jiti@2.7.0)(sass@1.99.0)(terser@5.47.1)(yaml@2.9.0)) '@nuxt/schema': 3.21.10 '@nuxt/telemetry': 2.8.0(@nuxt/kit@3.21.10(magicast@0.5.2)) - '@nuxt/vite-builder': 3.21.10(@types/node@22.19.17)(eslint@9.39.4(jiti@2.7.0))(lightningcss@1.33.0)(magicast@0.5.2)(nuxt@3.21.10(@electric-sql/pglite@0.4.1)(@oxc-project/types@0.140.0)(@parcel/watcher@2.5.6)(@types/node@22.19.17)(@vue/compiler-sfc@3.5.41)(cac@6.7.14)(db0@0.3.4(@electric-sql/pglite@0.4.1)(mysql2@3.15.3))(esbuild@0.28.0)(eslint@9.39.4(jiti@2.7.0))(ioredis@5.10.1)(lightningcss@1.33.0)(magicast@0.5.2)(mysql2@3.15.3)(optionator@0.9.4)(rolldown@1.1.5)(rollup-plugin-visualizer@7.0.1(rolldown@1.1.5)(rollup@4.62.4))(rollup@4.62.4)(sass@1.99.0)(terser@5.47.1)(typescript@5.9.3)(vite@8.1.5(@types/node@22.19.17)(esbuild@0.28.0)(jiti@2.7.0)(sass@1.99.0)(terser@5.47.1)(yaml@2.9.0))(vue-tsc@3.2.6(typescript@5.9.3))(yaml@2.9.0))(optionator@0.9.4)(rolldown@1.1.5)(rollup-plugin-visualizer@7.0.1(rolldown@1.1.5)(rollup@4.62.4))(rollup@4.62.4)(sass@1.99.0)(terser@5.47.1)(typescript@5.9.3)(vue-tsc@3.2.6(typescript@5.9.3))(vue@3.5.41(typescript@5.9.3))(yaml@2.9.0) + '@nuxt/vite-builder': 3.21.10(@types/node@22.19.17)(eslint@9.39.4(jiti@2.7.0))(lightningcss@1.33.0)(magicast@0.5.2)(nuxt@3.21.10(@electric-sql/pglite@0.4.1)(@oxc-project/types@0.143.0)(@parcel/watcher@2.5.6)(@types/node@22.19.17)(@vue/compiler-sfc@3.5.41)(cac@6.7.14)(db0@0.3.4(@electric-sql/pglite@0.4.1)(mysql2@3.15.3))(esbuild@0.28.0)(eslint@9.39.4(jiti@2.7.0))(ioredis@5.10.1)(lightningcss@1.33.0)(magicast@0.5.2)(mysql2@3.15.3)(optionator@0.9.4)(rolldown@1.1.5)(rollup-plugin-visualizer@7.0.1(rolldown@1.1.5)(rollup@4.62.4))(rollup@4.62.4)(sass@1.99.0)(terser@5.47.1)(typescript@5.9.3)(vite@8.1.5(@types/node@22.19.17)(esbuild@0.28.0)(jiti@2.7.0)(sass@1.99.0)(terser@5.47.1)(yaml@2.9.0))(vue-tsc@3.2.6(typescript@5.9.3))(yaml@2.9.0))(optionator@0.9.4)(rolldown@1.1.5)(rollup-plugin-visualizer@7.0.1(rolldown@1.1.5)(rollup@4.62.4))(rollup@4.62.4)(sass@1.99.0)(terser@5.47.1)(typescript@5.9.3)(vue-tsc@3.2.6(typescript@5.9.3))(vue@3.5.41(typescript@5.9.3))(yaml@2.9.0) '@unhead/vue': 2.1.17(vue@3.5.41(typescript@5.9.3)) '@vue/shared': 3.5.41 c12: 3.3.4(magicast@0.5.2) @@ -22591,7 +23345,7 @@ snapshots: oxc-minify: 0.141.0 oxc-parser: 0.140.0 oxc-transform: 0.141.0 - oxc-walker: 1.1.1(@oxc-project/types@0.140.0)(oxc-parser@0.140.0)(rolldown@1.1.5) + oxc-walker: 1.1.1(@oxc-project/types@0.143.0)(oxc-parser@0.140.0)(rolldown@1.1.5) pathe: 2.0.3 perfect-debounce: 2.1.0 pkg-types: 2.3.1 @@ -22681,16 +23435,16 @@ snapshots: - xml2js - yaml - nuxt@3.21.10(@electric-sql/pglite@0.4.1)(@oxc-project/types@0.140.0)(@parcel/watcher@2.5.6)(@types/node@24.13.3)(@vue/compiler-sfc@3.5.41)(cac@7.0.0)(db0@0.3.4(@electric-sql/pglite@0.4.1)(mysql2@3.15.3))(esbuild@0.28.1)(eslint@9.39.4(jiti@2.7.0))(ioredis@5.10.1)(lightningcss@1.33.0)(magicast@0.5.4)(mysql2@3.15.3)(optionator@0.9.4)(rolldown@1.1.5)(rollup-plugin-visualizer@7.0.1(rolldown@1.1.5)(rollup@4.62.4))(rollup@4.62.4)(sass@1.99.0)(terser@5.47.1)(typescript@5.9.3)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.99.0)(terser@5.47.1)(yaml@2.9.0))(vue-tsc@3.2.6(typescript@5.9.3))(yaml@2.9.0): + nuxt@3.21.10(@electric-sql/pglite@0.4.1)(@oxc-project/types@0.143.0)(@parcel/watcher@2.5.6)(@types/node@24.13.3)(@vue/compiler-sfc@3.5.41)(cac@7.0.0)(db0@0.3.4(@electric-sql/pglite@0.4.1)(mysql2@3.15.3))(esbuild@0.28.1)(eslint@9.39.4(jiti@2.7.0))(ioredis@5.10.1)(lightningcss@1.33.0)(magicast@0.5.4)(mysql2@3.15.3)(optionator@0.9.4)(rolldown@1.1.5)(rollup-plugin-visualizer@7.0.1(rolldown@1.1.5)(rollup@4.62.4))(rollup@4.62.4)(sass@1.99.0)(terser@5.47.1)(typescript@5.9.3)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.99.0)(terser@5.47.1)(yaml@2.9.0))(vue-tsc@3.2.6(typescript@5.9.3))(yaml@2.9.0): dependencies: '@dxup/nuxt': 0.5.6(esbuild@0.28.1)(magicast@0.5.4)(oxc-parser@0.140.0)(rolldown@1.1.5)(rollup@4.62.4)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.99.0)(terser@5.47.1)(yaml@2.9.0)) '@nuxt/cli': 3.37.0(@nuxt/schema@3.21.10)(@parcel/watcher@2.5.6)(cac@7.0.0)(magicast@0.5.4) '@nuxt/devtools': 3.4.1(db0@0.3.4(@electric-sql/pglite@0.4.1)(mysql2@3.15.3))(ioredis@5.10.1)(magic-string@0.30.21)(oxc-parser@0.140.0)(rolldown@1.1.5)(unplugin@3.3.0(esbuild@0.28.1)(rolldown@1.1.5)(rollup@4.62.4)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.99.0)(terser@5.47.1)(yaml@2.9.0)))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.99.0)(terser@5.47.1)(yaml@2.9.0))(vue@3.5.41(typescript@5.9.3)) '@nuxt/kit': 3.21.10(magicast@0.5.4) - '@nuxt/nitro-server': 3.21.10(@electric-sql/pglite@0.4.1)(db0@0.3.4(@electric-sql/pglite@0.4.1)(mysql2@3.15.3))(esbuild@0.28.1)(ioredis@5.10.1)(magicast@0.5.4)(mysql2@3.15.3)(nuxt@3.21.10(@electric-sql/pglite@0.4.1)(@oxc-project/types@0.140.0)(@parcel/watcher@2.5.6)(@types/node@24.13.3)(@vue/compiler-sfc@3.5.41)(cac@7.0.0)(db0@0.3.4(@electric-sql/pglite@0.4.1)(mysql2@3.15.3))(esbuild@0.28.1)(eslint@9.39.4(jiti@2.7.0))(ioredis@5.10.1)(lightningcss@1.33.0)(magicast@0.5.4)(mysql2@3.15.3)(optionator@0.9.4)(rolldown@1.1.5)(rollup-plugin-visualizer@7.0.1(rolldown@1.1.5)(rollup@4.62.4))(rollup@4.62.4)(sass@1.99.0)(terser@5.47.1)(typescript@5.9.3)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.99.0)(terser@5.47.1)(yaml@2.9.0))(vue-tsc@3.2.6(typescript@5.9.3))(yaml@2.9.0))(oxc-parser@0.140.0)(rolldown@1.1.5)(rollup@4.62.4)(typescript@5.9.3)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.99.0)(terser@5.47.1)(yaml@2.9.0)) + '@nuxt/nitro-server': 3.21.10(@electric-sql/pglite@0.4.1)(db0@0.3.4(@electric-sql/pglite@0.4.1)(mysql2@3.15.3))(esbuild@0.28.1)(ioredis@5.10.1)(magicast@0.5.4)(mysql2@3.15.3)(nuxt@3.21.10(@electric-sql/pglite@0.4.1)(@oxc-project/types@0.143.0)(@parcel/watcher@2.5.6)(@types/node@24.13.3)(@vue/compiler-sfc@3.5.41)(cac@7.0.0)(db0@0.3.4(@electric-sql/pglite@0.4.1)(mysql2@3.15.3))(esbuild@0.28.1)(eslint@9.39.4(jiti@2.7.0))(ioredis@5.10.1)(lightningcss@1.33.0)(magicast@0.5.4)(mysql2@3.15.3)(optionator@0.9.4)(rolldown@1.1.5)(rollup-plugin-visualizer@7.0.1(rolldown@1.1.5)(rollup@4.62.4))(rollup@4.62.4)(sass@1.99.0)(terser@5.47.1)(typescript@5.9.3)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.99.0)(terser@5.47.1)(yaml@2.9.0))(vue-tsc@3.2.6(typescript@5.9.3))(yaml@2.9.0))(oxc-parser@0.140.0)(rolldown@1.1.5)(rollup@4.62.4)(typescript@5.9.3)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.99.0)(terser@5.47.1)(yaml@2.9.0)) '@nuxt/schema': 3.21.10 '@nuxt/telemetry': 2.8.0(@nuxt/kit@3.21.10(magicast@0.5.4)) - '@nuxt/vite-builder': 3.21.10(@types/node@24.13.3)(eslint@9.39.4(jiti@2.7.0))(lightningcss@1.33.0)(magicast@0.5.4)(nuxt@3.21.10(@electric-sql/pglite@0.4.1)(@oxc-project/types@0.140.0)(@parcel/watcher@2.5.6)(@types/node@24.13.3)(@vue/compiler-sfc@3.5.41)(cac@7.0.0)(db0@0.3.4(@electric-sql/pglite@0.4.1)(mysql2@3.15.3))(esbuild@0.28.1)(eslint@9.39.4(jiti@2.7.0))(ioredis@5.10.1)(lightningcss@1.33.0)(magicast@0.5.4)(mysql2@3.15.3)(optionator@0.9.4)(rolldown@1.1.5)(rollup-plugin-visualizer@7.0.1(rolldown@1.1.5)(rollup@4.62.4))(rollup@4.62.4)(sass@1.99.0)(terser@5.47.1)(typescript@5.9.3)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.99.0)(terser@5.47.1)(yaml@2.9.0))(vue-tsc@3.2.6(typescript@5.9.3))(yaml@2.9.0))(optionator@0.9.4)(rolldown@1.1.5)(rollup-plugin-visualizer@7.0.1(rolldown@1.1.5)(rollup@4.62.4))(rollup@4.62.4)(sass@1.99.0)(terser@5.47.1)(typescript@5.9.3)(vue-tsc@3.2.6(typescript@5.9.3))(vue@3.5.41(typescript@5.9.3))(yaml@2.9.0) + '@nuxt/vite-builder': 3.21.10(@types/node@24.13.3)(eslint@9.39.4(jiti@2.7.0))(lightningcss@1.33.0)(magicast@0.5.4)(nuxt@3.21.10(@electric-sql/pglite@0.4.1)(@oxc-project/types@0.143.0)(@parcel/watcher@2.5.6)(@types/node@24.13.3)(@vue/compiler-sfc@3.5.41)(cac@7.0.0)(db0@0.3.4(@electric-sql/pglite@0.4.1)(mysql2@3.15.3))(esbuild@0.28.1)(eslint@9.39.4(jiti@2.7.0))(ioredis@5.10.1)(lightningcss@1.33.0)(magicast@0.5.4)(mysql2@3.15.3)(optionator@0.9.4)(rolldown@1.1.5)(rollup-plugin-visualizer@7.0.1(rolldown@1.1.5)(rollup@4.62.4))(rollup@4.62.4)(sass@1.99.0)(terser@5.47.1)(typescript@5.9.3)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.99.0)(terser@5.47.1)(yaml@2.9.0))(vue-tsc@3.2.6(typescript@5.9.3))(yaml@2.9.0))(optionator@0.9.4)(rolldown@1.1.5)(rollup-plugin-visualizer@7.0.1(rolldown@1.1.5)(rollup@4.62.4))(rollup@4.62.4)(sass@1.99.0)(terser@5.47.1)(typescript@5.9.3)(vue-tsc@3.2.6(typescript@5.9.3))(vue@3.5.41(typescript@5.9.3))(yaml@2.9.0) '@unhead/vue': 2.1.17(vue@3.5.41(typescript@5.9.3)) '@vue/shared': 3.5.41 c12: 3.3.4(magicast@0.5.4) @@ -22721,7 +23475,7 @@ snapshots: oxc-minify: 0.141.0 oxc-parser: 0.140.0 oxc-transform: 0.141.0 - oxc-walker: 1.1.1(@oxc-project/types@0.140.0)(oxc-parser@0.140.0)(rolldown@1.1.5) + oxc-walker: 1.1.1(@oxc-project/types@0.143.0)(oxc-parser@0.140.0)(rolldown@1.1.5) pathe: 2.0.3 perfect-debounce: 2.1.0 pkg-types: 2.3.1 @@ -23013,6 +23767,30 @@ snapshots: '@oxc-parser/binding-win32-ia32-msvc': 0.140.0 '@oxc-parser/binding-win32-x64-msvc': 0.140.0 + oxc-parser@0.143.0: + dependencies: + '@oxc-project/types': 0.143.0 + optionalDependencies: + '@oxc-parser/binding-android-arm-eabi': 0.143.0 + '@oxc-parser/binding-android-arm64': 0.143.0 + '@oxc-parser/binding-darwin-arm64': 0.143.0 + '@oxc-parser/binding-darwin-x64': 0.143.0 + '@oxc-parser/binding-freebsd-x64': 0.143.0 + '@oxc-parser/binding-linux-arm-gnueabihf': 0.143.0 + '@oxc-parser/binding-linux-arm-musleabihf': 0.143.0 + '@oxc-parser/binding-linux-arm64-gnu': 0.143.0 + '@oxc-parser/binding-linux-arm64-musl': 0.143.0 + '@oxc-parser/binding-linux-ppc64-gnu': 0.143.0 + '@oxc-parser/binding-linux-riscv64-gnu': 0.143.0 + '@oxc-parser/binding-linux-riscv64-musl': 0.143.0 + '@oxc-parser/binding-linux-s390x-gnu': 0.143.0 + '@oxc-parser/binding-linux-x64-gnu': 0.143.0 + '@oxc-parser/binding-linux-x64-musl': 0.143.0 + '@oxc-parser/binding-openharmony-arm64': 0.143.0 + '@oxc-parser/binding-win32-arm64-msvc': 0.143.0 + '@oxc-parser/binding-win32-ia32-msvc': 0.143.0 + '@oxc-parser/binding-win32-x64-msvc': 0.143.0 + oxc-parser@0.70.0: dependencies: '@oxc-project/types': 0.70.0 @@ -23032,6 +23810,28 @@ snapshots: '@oxc-parser/binding-win32-arm64-msvc': 0.70.0 '@oxc-parser/binding-win32-x64-msvc': 0.70.0 + oxc-resolver@11.24.2: + optionalDependencies: + '@oxc-resolver/binding-android-arm-eabi': 11.24.2 + '@oxc-resolver/binding-android-arm64': 11.24.2 + '@oxc-resolver/binding-darwin-arm64': 11.24.2 + '@oxc-resolver/binding-darwin-x64': 11.24.2 + '@oxc-resolver/binding-freebsd-x64': 11.24.2 + '@oxc-resolver/binding-linux-arm-gnueabihf': 11.24.2 + '@oxc-resolver/binding-linux-arm-musleabihf': 11.24.2 + '@oxc-resolver/binding-linux-arm64-gnu': 11.24.2 + '@oxc-resolver/binding-linux-arm64-musl': 11.24.2 + '@oxc-resolver/binding-linux-ppc64-gnu': 11.24.2 + '@oxc-resolver/binding-linux-riscv64-gnu': 11.24.2 + '@oxc-resolver/binding-linux-riscv64-musl': 11.24.2 + '@oxc-resolver/binding-linux-s390x-gnu': 11.24.2 + '@oxc-resolver/binding-linux-x64-gnu': 11.24.2 + '@oxc-resolver/binding-linux-x64-musl': 11.24.2 + '@oxc-resolver/binding-openharmony-arm64': 11.24.2 + '@oxc-resolver/binding-wasm32-wasi': 11.24.2 + '@oxc-resolver/binding-win32-arm64-msvc': 11.24.2 + '@oxc-resolver/binding-win32-x64-msvc': 11.24.2 + oxc-transform@0.141.0: optionalDependencies: '@oxc-transform/binding-android-arm-eabi': 0.141.0 @@ -23055,9 +23855,9 @@ snapshots: '@oxc-transform/binding-win32-ia32-msvc': 0.141.0 '@oxc-transform/binding-win32-x64-msvc': 0.141.0 - oxc-walker@1.1.1(@oxc-project/types@0.140.0)(oxc-parser@0.140.0)(rolldown@1.1.5): + oxc-walker@1.1.1(@oxc-project/types@0.143.0)(oxc-parser@0.140.0)(rolldown@1.1.5): optionalDependencies: - '@oxc-project/types': 0.140.0 + '@oxc-project/types': 0.143.0 oxc-parser: 0.140.0 rolldown: 1.1.5 @@ -23638,6 +24438,10 @@ snapshots: dependencies: prettier: 3.8.3 + prettier-plugin-sort-json@4.2.0(prettier@3.9.6): + dependencies: + prettier: 3.9.6 + prettier-plugin-tailwindcss@0.6.14(prettier-plugin-organize-imports@4.3.0(prettier@3.8.3)(typescript@5.9.3)(vue-tsc@3.2.6(typescript@5.9.3)))(prettier@3.8.3): dependencies: prettier: 3.8.3 @@ -23646,6 +24450,8 @@ snapshots: prettier@3.8.3: {} + prettier@3.9.6: {} + pretty-bytes@7.1.0: {} prisma@7.7.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(magicast@0.5.2)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@5.9.3): @@ -25410,6 +26216,8 @@ snapshots: ultrahtml@1.7.0: {} + unbash@4.0.10: {} + unbox-primitive@1.1.0: dependencies: call-bound: 1.0.4 @@ -25461,6 +26269,13 @@ snapshots: rolldown: 1.1.5 unplugin: 3.3.0(esbuild@0.28.1)(rolldown@1.1.5)(rollup@4.62.4)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.99.0)(terser@5.47.1)(yaml@2.9.0)) + unctx@3.0.0(magic-string@1.1.0)(oxc-parser@0.143.0)(rolldown@1.1.5)(unplugin@3.3.0(esbuild@0.28.0)(rolldown@1.1.5)(rollup@4.62.4)(vite@8.1.5(@types/node@22.19.17)(esbuild@0.28.0)(jiti@2.7.0)(sass@1.99.0)(terser@5.47.1)(yaml@2.9.0))): + optionalDependencies: + magic-string: 1.1.0 + oxc-parser: 0.143.0 + rolldown: 1.1.5 + unplugin: 3.3.0(esbuild@0.28.0)(rolldown@1.1.5)(rollup@4.62.4)(vite@8.1.5(@types/node@22.19.17)(esbuild@0.28.0)(jiti@2.7.0)(sass@1.99.0)(terser@5.47.1)(yaml@2.9.0)) + undici-types@6.21.0: {} undici-types@7.16.0: {} @@ -25587,6 +26402,35 @@ snapshots: - vite - webpack + unimport@6.2.0(esbuild@0.28.0)(oxc-parser@0.143.0)(rolldown@1.1.5)(rollup@4.60.3)(vite@8.1.5(@types/node@22.19.17)(esbuild@0.28.0)(jiti@2.7.0)(sass@1.99.0)(terser@5.47.1)(yaml@2.9.0)): + dependencies: + acorn: 8.16.0 + escape-string-regexp: 5.0.0 + estree-walker: 3.0.3 + local-pkg: 1.1.2 + magic-string: 0.30.21 + mlly: 1.8.2 + pathe: 2.0.3 + picomatch: 4.0.5 + pkg-types: 2.3.1 + scule: 1.3.0 + strip-literal: 3.1.0 + tinyglobby: 0.2.17 + unplugin: 3.3.0(esbuild@0.28.0)(rolldown@1.1.5)(rollup@4.60.3)(vite@8.1.5(@types/node@22.19.17)(esbuild@0.28.0)(jiti@2.7.0)(sass@1.99.0)(terser@5.47.1)(yaml@2.9.0)) + unplugin-utils: 0.3.1 + optionalDependencies: + oxc-parser: 0.143.0 + transitivePeerDependencies: + - '@farmfe/core' + - '@rspack/core' + - bun-types-no-globals + - esbuild + - rolldown + - rollup + - unloader + - vite + - webpack + unimport@6.4.0(esbuild@0.28.0)(oxc-parser@0.140.0)(rolldown@1.1.5)(rollup@4.62.4)(vite@8.1.5(@types/node@22.19.17)(esbuild@0.28.0)(jiti@2.7.0)(sass@1.99.0)(terser@5.47.1)(yaml@2.9.0)): dependencies: acorn: 8.18.0 @@ -26357,6 +27201,8 @@ snapshots: dependencies: xml-name-validator: 3.0.0 + walk-up-path@4.0.0: {} + web-namespaces@2.0.1: {} web-worker@1.5.0: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 209ed10cc..41e5beaff 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -5,6 +5,7 @@ packages: - "desktop/" onlyBuiltDependencies: + - "@ast-grep/cli" - "@bufbuild/buf" - "@parcel/watcher" - "@prisma/client" @@ -17,6 +18,7 @@ onlyBuiltDependencies: - unrs-resolver - contentlayer - contentlayer2 + - lefthook - protobufjs overrides: diff --git a/rules/no-console-log.yml b/rules/no-console-log.yml new file mode 100644 index 000000000..2dc0e2a3d --- /dev/null +++ b/rules/no-console-log.yml @@ -0,0 +1,12 @@ +id: no-console-log +message: Use the project logger (pino) instead of console.log +severity: warning # promote to error once codebase is clean +language: TypeScript +rule: + pattern: console.log($$$ARGS) +files: + - server/** +ignores: + - "**/*.d.ts" + - "**/*.spec.ts" + - "**/*.test.ts" diff --git a/rules/no-fmt-println.yml b/rules/no-fmt-println.yml new file mode 100644 index 000000000..3f0f53b78 --- /dev/null +++ b/rules/no-fmt-println.yml @@ -0,0 +1,6 @@ +id: no-fmt-println +message: Use a structured logger instead of fmt.Println +severity: warning +language: Go +rule: + pattern: fmt.Println($$$ARGS) diff --git a/scripts/clippy-changed.sh b/scripts/clippy-changed.sh new file mode 100755 index 000000000..55c654505 --- /dev/null +++ b/scripts/clippy-changed.sh @@ -0,0 +1,46 @@ +#!/usr/bin/env bash +# Run `cargo clippy -D warnings` only over crates that contain changed .rs files. +# Called by lefthook pre-push. desktop/src-tauri is intentionally excluded: +# it needs Tauri system libraries that contributors on minimal setups won't have. +# CI covers it in droplet-ci.yml / torrential-ci.yml. +set -uo pipefail + +# Resolve the repo root regardless of where the hook runs. +repo_root="$(git rev-parse --show-toplevel)" +cd "$repo_root" || exit 1 + +# :colon: curls are handled by set -e semantics below. +crates=( + cli + torrential + desktop/src-tauri + libraries/droplet + libraries/droplet_types + libraries/libarchive + libraries/native_model +) + +# Accumulate changed .rs files relative to repo root. +changed_rs="$(git diff --name-only HEAD -- '*.rs' 2>/dev/null; git diff --cached --name-only -- '*.rs' 2>/dev/null)" +# Falls back to previous-branch commit when @{push} isn't resolvable. +if [ -z "$changed_rs" ]; then + upstream="$(git rev-parse --verify '@{push}' 2>/dev/null || git rev-parse --verify 'HEAD~1' 2>/dev/null || echo "")" + if [ -n "$upstream" ]; then + changed_rs="$(git diff --name-only "$upstream..HEAD" -- '*.rs' 2>/dev/null)" + fi +fi + +if [ -z "$changed_rs" ]; then + echo "clippy-changed: no .rs changes; skipping" + exit 0 +fi + +status=0 +for dir in "${crates[@]}"; do + if printf '%s\n' "$changed_rs" | grep -q "^${dir}/"; then + echo "==> cargo clippy (${dir})" + (cd "$dir" && cargo clippy --all-targets --all-features -- -D warnings) || status=1 + fi +done + +exit "$status" \ No newline at end of file diff --git a/sgconfig.yml b/sgconfig.yml new file mode 100644 index 000000000..4e9f32a39 --- /dev/null +++ b/sgconfig.yml @@ -0,0 +1,5 @@ +# ast-grep configuration. Rules live in ./rules. +# Scan: pnpm exec ast-grep scan [paths] +# Machine-readable: pnpm exec ast-grep scan --json +ruleDirs: + - rules From ab351bf800feba2e5658c9758fe3f5682a501497 Mon Sep 17 00:00:00 2001 From: John Smith Date: Wed, 26 Aug 2026 01:07:13 -0400 Subject: [PATCH 002/243] style: apply root prettier config across repo First repo-wide pass with the new shared .prettierrc.json (copied from server/): adds missing final newlines and normalizes quote style. No functional changes. Co-Authored-By: Claude Opus 5 --- .github/workflows/client-release.yml | 5 +- cli/README.md | 2 +- cli/spec.md | 3 + desktop/DEBUG.md | 2 + desktop/README.md | 2 +- desktop/build.mjs | 2 +- desktop/changelog.md | 18 +- .../components/DependencyRequiredModal.vue | 4 +- .../components/GameOptions/ProtonSelector.vue | 6 +- .../main/components/GameOptions/Updates.vue | 3 +- desktop/main/components/GameOptionsModal.vue | 4 +- desktop/main/components/Header.vue | 2 +- desktop/main/components/HeaderButton.vue | 10 +- .../components/HeaderProtonSupportWidget.vue | 5 +- desktop/main/components/HeaderUserWidget.vue | 2 +- .../main/components/InitiateAuthModule.vue | 8 +- desktop/main/components/LibrarySearch.vue | 9 +- desktop/main/components/Logo.vue | 21 +- desktop/main/components/Wordmark.vue | 27 +- desktop/main/composables/app-state.ts | 2 +- .../main/composables/current-page-engine.ts | 11 +- desktop/main/composables/game.ts | 8 +- desktop/main/composables/generateGameMeta.ts | 19 +- desktop/main/composables/state-navigation.ts | 6 +- desktop/main/error.vue | 1 - desktop/main/nuxt.config.ts | 2 +- desktop/main/pages/community.vue | 4 +- .../main/pages/error/serverunavailable.vue | 2 +- desktop/main/pages/index.vue | 4 +- desktop/main/pages/library/[id]/index.vue | 8 +- desktop/main/pages/library/index.vue | 31 +- desktop/main/pages/news.vue | 4 +- desktop/main/pages/settings/downloads.vue | 189 +- desktop/main/pages/setup/index.vue | 4 +- desktop/main/pages/setup/server.vue | 7 +- desktop/main/pnpm-lock.yaml | 7879 +++++++++++------ desktop/main/pnpm-workspace.yaml | 26 +- desktop/main/tailwind.config.js | 4 +- desktop/main/tsconfig.json | 4 +- desktop/main/types.ts | 2 +- desktop/src-tauri/capabilities/default.json | 10 +- desktop/src-tauri/tauri.conf.json | 54 +- libraries/base/components/LoadingButton.vue | 3 +- libraries/base/components/ModalTemplate.vue | 11 +- .../base/components/NotificationModal.vue | 65 +- libraries/base/composables/modal-stack.ts | 18 +- libraries/base/eslint.config.js | 4 +- libraries/droplet/README.md | 3 +- libraries/droplet_types/README.md | 3 +- libraries/libarchive/.travis.yml | 8 +- libraries/libarchive/README.md | 2 + .../workflows/build_and_test_release.yml | 10 +- .../.github/workflows/clippy_check.yml | 12 +- .../workflows/conventional_commits.yml | 4 +- .../.github/workflows/fmt_check.yml | 12 +- libraries/native_model/README.md | 53 +- .../native_model/native_model_macro/README.md | 2 +- libraries/native_model/renovate.json | 43 +- server/components/GameEditor/Version.vue | 4 +- server/composables/admin-home.ts | 15 +- server/server/internal/auth/oidc/index.ts | 3 +- server/server/internal/config/sys-conf.ts | 3 +- server/server/internal/tasks/index.ts | 3 +- sites/docs/.github/workflows/deploy.yml | 10 +- sites/docs/.vscode/launch.json | 4 +- sites/docs/LICENSE.md | 140 +- sites/docs/README.md | 2 +- sites/docs/package.json | 2 +- sites/docs/src/content.config.ts | 8 +- .../content/docs/admin/authentication/mfa.md | 8 +- sites/docs/src/content/docs/admin/index.md | 2 +- .../src/content/docs/admin/metadata/manual.md | 2 +- .../docs/src/content/docs/admin/quickstart.md | 1 + .../src/content/docs/user/install/debian.md | 1 + .../src/content/docs/user/install/fedora.md | 1 + sites/docs/tsconfig.json | 4 +- sites/promo/LICENSE.md | 312 +- sites/promo/src/app/about/page.tsx | 4 +- sites/promo/src/app/comparison/page.tsx | 15 +- sites/promo/src/app/download/page.tsx | 3 +- sites/promo/src/app/gallery/page.tsx | 1 - sites/promo/src/app/layout.tsx | 4 +- sites/promo/src/app/news/page.tsx | 6 +- sites/promo/src/app/not-found.tsx | 4 +- sites/promo/src/app/page.tsx | 4 +- sites/promo/src/app/sponsors/page.tsx | 5 +- sites/promo/src/components/footer.tsx | 13 +- sites/promo/src/components/gradient.tsx | 2 +- sites/promo/src/components/logo.tsx | 2 +- sites/promo/src/components/mdx-components.tsx | 4 +- sites/promo/src/components/news.tsx | 21 +- sites/promo/src/components/screenshot.tsx | 5 +- sites/promo/src/components/sponsors.tsx | 4 +- sites/promo/src/components/text.tsx | 2 +- sites/promo/src/styles/tailwind.css | 2 +- sites/promo/tsconfig.json | 24 +- torrential/LICENSE.md | 140 +- torrential/README.md | 3 +- torrential/docs/protocol.md | 7 +- torrential/docs/structure.md | 5 +- 100 files changed, 6105 insertions(+), 3369 deletions(-) diff --git a/.github/workflows/client-release.yml b/.github/workflows/client-release.yml index ee511a244..45d437c6b 100644 --- a/.github/workflows/client-release.yml +++ b/.github/workflows/client-release.yml @@ -51,7 +51,6 @@ jobs: node-version: lts/* cache: pnpm - - name: install Rust nightly uses: dtolnay/rust-toolchain@nightly with: @@ -61,7 +60,7 @@ jobs: - name: Rust cache uses: swatinem/rust-cache@v2 with: - workspaces: './desktop/src-tauri -> target' + workspaces: "./desktop/src-tauri -> target" - name: install dependencies (ubuntu only) if: matrix.platform == 'ubuntu-22.04' || matrix.platform == 'ubuntu-22.04-arm' # This must match the platform value defined above. @@ -137,4 +136,4 @@ jobs: releaseDraft: false prerelease: true args: ${{ matrix.args }} - projectPath: './desktop' \ No newline at end of file + projectPath: "./desktop" diff --git a/cli/README.md b/cli/README.md index 77643fd94..6f2c804d2 100644 --- a/cli/README.md +++ b/cli/README.md @@ -1,3 +1,3 @@ # CLI (`downpour`) -The cli way to access Drop. Used for admin tasks that require local access, like uploading game content. \ No newline at end of file +The cli way to access Drop. Used for admin tasks that require local access, like uploading game content. diff --git a/cli/spec.md b/cli/spec.md index e54017505..debad101d 100644 --- a/cli/spec.md +++ b/cli/spec.md @@ -1,6 +1,9 @@ # Downpour CLI spec + `downpour [command] --opts` + ## Commands: + - new - creates/initalizes a depot at the endpoint. Creates manifest.json and speedtest - connect [name] - connects to an s3 endpoint and saves the endpoint to some sort of credentials file. Name is either as provided or the hostname of the endpoint - upload - uploads game as described before. Should fail if depot isn't initialized with new from above diff --git a/desktop/DEBUG.md b/desktop/DEBUG.md index 2a4478f36..f84ed785b 100644 --- a/desktop/DEBUG.md +++ b/desktop/DEBUG.md @@ -1,6 +1,7 @@ # How to create Flamegraph Run this in `src-tauri`: + ``` WEBKIT_DISABLE_DMABUF_RENDERER=1 CARGO_PROFILE_RELEASE_DEBUG=true cargo flamegraph --release ``` @@ -8,6 +9,7 @@ WEBKIT_DISABLE_DMABUF_RENDERER=1 CARGO_PROFILE_RELEASE_DEBUG=true cargo flamegra You can leave out `WEBKIT_DISABLE_DMABUF_RENDERER=1` if you're not on NVIDIA/Linux And then run this in the root dir: + ``` yarn dev --port 1432 ``` diff --git a/desktop/README.md b/desktop/README.md index 87a26cc3b..111effa15 100644 --- a/desktop/README.md +++ b/desktop/README.md @@ -1,3 +1,3 @@ # Desktop -The official desktop client for Drop. \ No newline at end of file +The official desktop client for Drop. diff --git a/desktop/build.mjs b/desktop/build.mjs index aeb341d11..2014d024a 100644 --- a/desktop/build.mjs +++ b/desktop/build.mjs @@ -45,4 +45,4 @@ for (const view of views) { fs.cpSync(`./${view}/.output/public`, `${OUTPUT}/${view}`, { recursive: true, }); -} \ No newline at end of file +} diff --git a/desktop/changelog.md b/desktop/changelog.md index 691c4601f..bc1d9e1a1 100644 --- a/desktop/changelog.md +++ b/desktop/changelog.md @@ -1,8 +1,7 @@ - - ## Release 0.2.0-beta ### Fixes + - Re-enabled killing games #005bab2 - fixed queue manipulation and waiting for downloads #01260f0 - fix logic error in detecting dir #04368ff @@ -49,8 +48,8 @@ - update readme instructions #f0c47d8 - Adding usize to completed_contexts_lock instead of &usize #f508186 - ### Features + - Game kill tauri command #01e6162 - add debug page #02f8591 - Add signout functionality (#16) #0a0d9d6 @@ -113,9 +112,9 @@ - add note about more install dirs #f4ac1c8 - Added rolling progress window #fd30b3e - ### Other Changes -- quexeky + +- quexeky - Convert DATA_ROOT_DIR to Mutex #00b7179 - Converting DB access to a trait #01b092c - Updated changelog #022330b @@ -284,12 +283,12 @@ - initial commit #f6cd7c3 - Update .gitlab-ci.yml #fc6bab9 - _changelog generated by_ [go-conventional-commits](https://github.com/joselitofilho/go-conventional-commits) ## Release 0.1.0-beta ### Fixes + - fixed queue manipulation and waiting for downloads #01260f0 - fix logic error in detecting dir #04368ff - absolute executable invoke #17759c4 @@ -307,8 +306,8 @@ _changelog generated by_ [go-conventional-commits](https://github.com/joselitofi - fix scrollbars on edge webview #f09605a - update readme instructions #f0c47d8 - ### Features + - queue and library UIs #0a20139 - add pre-launch log to file #17f8d76 - Added option to change root directory #1aa52c0 @@ -343,9 +342,9 @@ _changelog generated by_ [go-conventional-commits](https://github.com/joselitofi - Generic function to set download state #f10d92d - Convert DownloadThreadControlFlag to AtomicBool #f25bfed - ### Other Changes -- quexeky + +- quexeky - Convert DATA_ROOT_DIR to Mutex #00b7179 - Converting DB access to a trait #01b092c - Scoping changes and removing qualifications #046ba64 @@ -459,5 +458,4 @@ _changelog generated by_ [go-conventional-commits](https://github.com/joselitofi - fixes and patches for merged changes #f6476bc - initial commit #f6cd7c3 - _changelog generated by_ [go-conventional-commits](https://github.com/joselitofilho/go-conventional-commits) diff --git a/desktop/main/components/DependencyRequiredModal.vue b/desktop/main/components/DependencyRequiredModal.vue index ab805b652..a675a83f4 100644 --- a/desktop/main/components/DependencyRequiredModal.vue +++ b/desktop/main/components/DependencyRequiredModal.vue @@ -70,10 +70,10 @@ const versionOptions = await invoke>( "fetch_game_version_options", { gameId: game.id, - } + }, ); const version = versionOptions.find( - (v) => v.versionId === model.value.versionId + (v) => v.versionId === model.value.versionId, )!; const installDirs = await invoke("fetch_download_dir_stats"); diff --git a/desktop/main/components/GameOptions/ProtonSelector.vue b/desktop/main/components/GameOptions/ProtonSelector.vue index 39806d3a4..7ca8c79b5 100644 --- a/desktop/main/components/GameOptions/ProtonSelector.vue +++ b/desktop/main/components/GameOptions/ProtonSelector.vue @@ -1,9 +1,5 @@ diff --git a/desktop/main/components/GameOptionsModal.vue b/desktop/main/components/GameOptionsModal.vue index 9ae4130dd..cea301d20 100644 --- a/desktop/main/components/GameOptionsModal.vue +++ b/desktop/main/components/GameOptionsModal.vue @@ -92,7 +92,9 @@ const open = defineModel(); const props = defineProps<{ gameId: string }>(); const game = await useGame(props.gameId); -const configuration: Ref = ref(game.version.value!.userConfiguration); +const configuration: Ref = ref( + game.version.value!.userConfiguration, +); const hasWindows = !!( game.version.value!.setups.find((v) => v.platform === "Windows") ?? diff --git a/desktop/main/components/Header.vue b/desktop/main/components/Header.vue index 7af7cd23e..4a9e52d0a 100644 --- a/desktop/main/components/Header.vue +++ b/desktop/main/components/Header.vue @@ -43,7 +43,7 @@ - + diff --git a/desktop/main/components/HeaderButton.vue b/desktop/main/components/HeaderButton.vue index 0811c1326..82c06051f 100644 --- a/desktop/main/components/HeaderButton.vue +++ b/desktop/main/components/HeaderButton.vue @@ -1,5 +1,7 @@ \ No newline at end of file + + diff --git a/desktop/main/components/HeaderProtonSupportWidget.vue b/desktop/main/components/HeaderProtonSupportWidget.vue index 59ddec44c..ab3bae86b 100644 --- a/desktop/main/components/HeaderProtonSupportWidget.vue +++ b/desktop/main/components/HeaderProtonSupportWidget.vue @@ -1,8 +1,5 @@ diff --git a/desktop/main/composables/app-state.ts b/desktop/main/composables/app-state.ts index 7ad06aba1..b47cf9cb7 100644 --- a/desktop/main/composables/app-state.ts +++ b/desktop/main/composables/app-state.ts @@ -1,3 +1,3 @@ import type { AppState } from "~/types"; -export const useAppState = () => useState("state"); \ No newline at end of file +export const useAppState = () => useState("state"); diff --git a/desktop/main/composables/current-page-engine.ts b/desktop/main/composables/current-page-engine.ts index 0990195c0..1117d73cd 100644 --- a/desktop/main/composables/current-page-engine.ts +++ b/desktop/main/composables/current-page-engine.ts @@ -2,7 +2,7 @@ import type { RouteLocationNormalized } from "vue-router"; import type { NavigationItem } from "~/types"; export const useCurrentNavigationIndex = ( - navigation: Array + navigation: Array, ) => { const router = useRouter(); const route = useRoute(); @@ -26,7 +26,10 @@ export const useCurrentNavigationIndex = ( currentNavigation.value = calculateCurrentNavIndex(to); }); - return {currentNavigation, recalculateNavigation: () => { - currentNavigation.value = calculateCurrentNavIndex(route); - }}; + return { + currentNavigation, + recalculateNavigation: () => { + currentNavigation.value = calculateCurrentNavIndex(route); + }, + }; }; diff --git a/desktop/main/composables/game.ts b/desktop/main/composables/game.ts index 93e97af2d..c212414a0 100644 --- a/desktop/main/composables/game.ts +++ b/desktop/main/composables/game.ts @@ -8,8 +8,9 @@ import type { RawGameStatus, } from "~/types"; -const gameRegistry: { [key: string]: { game: Game; version: Ref } } = - {}; +const gameRegistry: { + [key: string]: { game: Game; version: Ref }; +} = {}; const gameStatusRegistry: { [key: string]: Ref } = {}; @@ -64,8 +65,7 @@ export const useGame = async (gameId: string) => { }; export type LaunchResult = - | { result: "Success" } - | { result: "InstallRequired"; data: [string, string] }; + { result: "Success" } | { result: "InstallRequired"; data: [string, string] }; export type VersionOption = { versionId: string; diff --git a/desktop/main/composables/generateGameMeta.ts b/desktop/main/composables/generateGameMeta.ts index fdfc4b7ab..8b332e78a 100644 --- a/desktop/main/composables/generateGameMeta.ts +++ b/desktop/main/composables/generateGameMeta.ts @@ -1,9 +1,12 @@ -import { type DownloadableMetadata, DownloadableType } from '~/types' +import { type DownloadableMetadata, DownloadableType } from "~/types"; -export default function generateGameMeta(gameId: string, version: string): DownloadableMetadata { - return { - id: gameId, - version, - downloadType: DownloadableType.Game - } -} \ No newline at end of file +export default function generateGameMeta( + gameId: string, + version: string, +): DownloadableMetadata { + return { + id: gameId, + version, + downloadType: DownloadableType.Game, + }; +} diff --git a/desktop/main/composables/state-navigation.ts b/desktop/main/composables/state-navigation.ts index 738fdf59c..be879dd28 100644 --- a/desktop/main/composables/state-navigation.ts +++ b/desktop/main/composables/state-navigation.ts @@ -13,7 +13,7 @@ export function setupHooks() { listen("auth/failed", (event) => { router.push( - `/auth/failed?error=${encodeURIComponent(event.payload as string)}` + `/auth/failed?error=${encodeURIComponent(event.payload as string)}`, ); }); @@ -32,7 +32,7 @@ export function setupHooks() { ).toString()}"`, buttonText: "Close", }, - (e, c) => c() + (e, c) => c(), ); }); @@ -51,7 +51,7 @@ export function setupHooks() { await invoke("open_process_logs", { gameId: event.payload }); } c(); - } + }, ); }); diff --git a/desktop/main/error.vue b/desktop/main/error.vue index f25c9e56a..2029e7937 100644 --- a/desktop/main/error.vue +++ b/desktop/main/error.vue @@ -7,7 +7,6 @@ class="mx-auto w-full max-w-7xl px-6 pt-6 sm:pt-10 lg:col-span-2 lg:col-start-1 lg:row-start-1 lg:px-8" > -
diff --git a/desktop/main/pages/error/serverunavailable.vue b/desktop/main/pages/error/serverunavailable.vue index cfbb1785e..8b645a237 100644 --- a/desktop/main/pages/error/serverunavailable.vue +++ b/desktop/main/pages/error/serverunavailable.vue @@ -22,7 +22,7 @@

    -
  • +
  • - +

    {{ dir }} @@ -38,12 +49,16 @@

    - @@ -63,9 +78,17 @@ Maximum Download Threads
    - +

    The maximum number of concurrent download threads. Higher values may @@ -75,60 +98,93 @@

    -

    Force Offline

    +

    + Force Offline +

    Drop will not make any external connections

    - - + +
    -
    -
    - -
    + +
    -
    - + + leave-to="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" + > + class="relative transform overflow-hidden rounded-lg bg-zinc-900 px-4 pb-4 pt-5 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:p-6" + >
    - +
    -
    - + ]" + > Add -
    -

    @@ -185,7 +252,7 @@ import { } from "@headlessui/vue"; import { FolderIcon, TrashIcon, XCircleIcon } from "@heroicons/vue/16/solid"; import { invoke } from "@tauri-apps/api/core"; -import { Switch } from '@headlessui/vue' +import { Switch } from "@headlessui/vue"; import { type Settings } from "~/types"; const open = ref(false); @@ -201,7 +268,7 @@ const forceOffline = ref(settings?.forceOffline ?? false); const saveState = reactive({ loading: false, - success: false + success: false, }); async function updateDirs() { @@ -264,7 +331,10 @@ async function saveSettings() { try { saveState.loading = true; await invoke("update_settings", { - newSettings: { maxDownloadThreads: downloadThreads.value, forceOffline: forceOffline.value }, + newSettings: { + maxDownloadThreads: downloadThreads.value, + forceOffline: forceOffline.value, + }, }); // Show success state @@ -274,9 +344,8 @@ async function saveSettings() { setTimeout(() => { saveState.success = false; }, 2000); - } catch (error) { - console.error('Failed to save settings:', error); + console.error("Failed to save settings:", error); } finally { saveState.loading = false; } @@ -284,15 +353,19 @@ async function saveSettings() { function validateNumberInput(event: KeyboardEvent) { // Allow only numbers and basic control keys - if (!/^\d$/.test(event.key) && - !['Backspace', 'Delete', 'Tab', 'ArrowLeft', 'ArrowRight'].includes(event.key)) { + if ( + !/^\d$/.test(event.key) && + !["Backspace", "Delete", "Tab", "ArrowLeft", "ArrowRight"].includes( + event.key, + ) + ) { event.preventDefault(); } } function validatePaste(event: ClipboardEvent) { // Prevent paste if content contains non-numeric characters - const pastedData = event.clipboardData?.getData('text'); + const pastedData = event.clipboardData?.getData("text"); if (pastedData && !/^\d+$/.test(pastedData)) { event.preventDefault(); } diff --git a/desktop/main/pages/setup/index.vue b/desktop/main/pages/setup/index.vue index 1b7eacb87..beddc6718 100644 --- a/desktop/main/pages/setup/index.vue +++ b/desktop/main/pages/setup/index.vue @@ -12,7 +12,9 @@ Let's get you set up

    - You'll need to have set up a Drop instance and created an account on it. If you're connecting to another person's instance, you'll need the url and an account. + You'll need to have set up a Drop instance and created an account on it. + If you're connecting to another person's instance, you'll need the url + and an account.

    { const prefixes = ["http://", "https://"]; - const doesntHavePrefix = prefixes.findIndex((e) => url.value.startsWith(e.slice(0, url.value.length))) == -1; + const doesntHavePrefix = + prefixes.findIndex((e) => + url.value.startsWith(e.slice(0, url.value.length)), + ) == -1; return doesntHavePrefix; -}) +}); async function connect() { const newUrl = url.value.startsWith("http") diff --git a/desktop/main/pnpm-lock.yaml b/desktop/main/pnpm-lock.yaml index 9d5295385..6c31ed418 100644 --- a/desktop/main/pnpm-lock.yaml +++ b/desktop/main/pnpm-lock.yaml @@ -1,49 +1,48 @@ -lockfileVersion: '9.0' +lockfileVersion: "9.0" settings: autoInstallPeers: true excludeLinksFromLockfile: false overrides: - devalue@>=5.1.0 <5.6.2: '>=5.6.2' - devalue@>=5.3.0 <=5.6.1: '>=5.6.2' - diff@>=6.0.0 <8.0.3: '>=8.0.3' - h3@<=1.15.4: '>=1.15.5' - lodash@>=4.0.0 <=4.17.22: '>=4.17.23' - markdown-it@>=13.0.0 <14.1.1: '>=14.1.1' - node-forge@<1.3.2: '>=1.3.2' - seroval@<1.4.1: '>=1.4.1' - seroval@<=1.4.0: '>=1.4.1' - tar@<7.5.7: '>=7.5.7' - tar@<=7.5.2: '>=7.5.3' - tar@<=7.5.3: '>=7.5.4' + devalue@>=5.1.0 <5.6.2: ">=5.6.2" + devalue@>=5.3.0 <=5.6.1: ">=5.6.2" + diff@>=6.0.0 <8.0.3: ">=8.0.3" + h3@<=1.15.4: ">=1.15.5" + lodash@>=4.0.0 <=4.17.22: ">=4.17.23" + markdown-it@>=13.0.0 <14.1.1: ">=14.1.1" + node-forge@<1.3.2: ">=1.3.2" + seroval@<1.4.1: ">=1.4.1" + seroval@<=1.4.0: ">=1.4.1" + tar@<7.5.7: ">=7.5.7" + tar@<=7.5.2: ">=7.5.3" + tar@<=7.5.3: ">=7.5.4" importers: - .: dependencies: - '@headlessui/vue': + "@headlessui/vue": specifier: ^1.7.23 version: 1.7.23(vue@3.5.41(typescript@5.9.3)) - '@heroicons/vue': + "@heroicons/vue": specifier: ^2.1.5 version: 2.2.0(vue@3.5.41(typescript@5.9.3)) - '@nuxtjs/tailwindcss': + "@nuxtjs/tailwindcss": specifier: ^6.12.2 version: 6.14.0(magicast@0.5.4)(yaml@2.9.0) - '@tauri-apps/api': + "@tauri-apps/api": specifier: ^2.9.1 version: 2.9.1 - '@tauri-apps/plugin-dialog': + "@tauri-apps/plugin-dialog": specifier: ^2.6.0 version: 2.6.0 - '@tauri-apps/plugin-os': + "@tauri-apps/plugin-os": specifier: ^2.3.2 version: 2.3.2 - '@tauri-apps/plugin-shell': + "@tauri-apps/plugin-shell": specifier: ^2.3.3 version: 2.3.3 - '@types/node': + "@types/node": specifier: ^25.5.0 version: 25.5.0 koa: @@ -68,13 +67,13 @@ importers: specifier: ^4.1.0 version: 4.1.0(vue@3.5.41(typescript@5.9.3)) devDependencies: - '@tailwindcss/forms': + "@tailwindcss/forms": specifier: ^0.5.9 version: 0.5.10(tailwindcss@3.4.18(yaml@2.9.0)) - '@tailwindcss/typography': + "@tailwindcss/typography": specifier: ^0.5.15 version: 0.5.19(tailwindcss@3.4.18(yaml@2.9.0)) - '@types/markdown-it': + "@types/markdown-it": specifier: ^14.1.2 version: 14.1.2 autoprefixer: @@ -97,160 +96,264 @@ importers: version: 2.2.12(typescript@5.9.3) packages: - - '@alloc/quick-lru@5.2.0': - resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} - engines: {node: '>=10'} - - '@babel/code-frame@7.29.7': - resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} - engines: {node: '>=6.9.0'} - - '@babel/compat-data@7.29.7': - resolution: {integrity: sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==} - engines: {node: '>=6.9.0'} - - '@babel/core@7.29.7': - resolution: {integrity: sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==} - engines: {node: '>=6.9.0'} - - '@babel/generator@7.29.8': - resolution: {integrity: sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==} - engines: {node: '>=6.9.0'} - - '@babel/generator@8.0.0': - resolution: {integrity: sha512-NT9NrVwJsbSV6Y2FSstWa71EETOnzrjkL5/wX3D2mYHtKM+qvqB1DvR4D0Setb/gDBsHzRICifwEWMO8CnTF6g==} - engines: {node: ^22.18.0 || >=24.11.0} - - '@babel/helper-annotate-as-pure@7.29.7': - resolution: {integrity: sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw==} - engines: {node: '>=6.9.0'} - - '@babel/helper-compilation-targets@7.29.7': - resolution: {integrity: sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==} - engines: {node: '>=6.9.0'} - - '@babel/helper-create-class-features-plugin@7.29.7': - resolution: {integrity: sha512-IY3ZD9Tmooqr3TUhc3DUWxiuo8xx1DWLhd5M7hQ+ZWJamqM2BbalrBJb2MisSLoYorOj75U03qULCxQTY9r3hg==} - engines: {node: '>=6.9.0'} + "@alloc/quick-lru@5.2.0": + resolution: + { + integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==, + } + engines: { node: ">=10" } + + "@babel/code-frame@7.29.7": + resolution: + { + integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==, + } + engines: { node: ">=6.9.0" } + + "@babel/compat-data@7.29.7": + resolution: + { + integrity: sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==, + } + engines: { node: ">=6.9.0" } + + "@babel/core@7.29.7": + resolution: + { + integrity: sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==, + } + engines: { node: ">=6.9.0" } + + "@babel/generator@7.29.8": + resolution: + { + integrity: sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==, + } + engines: { node: ">=6.9.0" } + + "@babel/generator@8.0.0": + resolution: + { + integrity: sha512-NT9NrVwJsbSV6Y2FSstWa71EETOnzrjkL5/wX3D2mYHtKM+qvqB1DvR4D0Setb/gDBsHzRICifwEWMO8CnTF6g==, + } + engines: { node: ^22.18.0 || >=24.11.0 } + + "@babel/helper-annotate-as-pure@7.29.7": + resolution: + { + integrity: sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw==, + } + engines: { node: ">=6.9.0" } + + "@babel/helper-compilation-targets@7.29.7": + resolution: + { + integrity: sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==, + } + engines: { node: ">=6.9.0" } + + "@babel/helper-create-class-features-plugin@7.29.7": + resolution: + { + integrity: sha512-IY3ZD9Tmooqr3TUhc3DUWxiuo8xx1DWLhd5M7hQ+ZWJamqM2BbalrBJb2MisSLoYorOj75U03qULCxQTY9r3hg==, + } + engines: { node: ">=6.9.0" } peerDependencies: - '@babel/core': ^7.0.0 - - '@babel/helper-globals@7.29.7': - resolution: {integrity: sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==} - engines: {node: '>=6.9.0'} - - '@babel/helper-member-expression-to-functions@7.29.7': - resolution: {integrity: sha512-j+7JYmk1JYDtACIGj0QJqqWZjoUpMoEikQGADMaHgCMCSDqd2+P32rfcibUNrGOMWrlzK1WJBdxrB3JJQZwWtg==} - engines: {node: '>=6.9.0'} - - '@babel/helper-module-imports@7.29.7': - resolution: {integrity: sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==} - engines: {node: '>=6.9.0'} - - '@babel/helper-module-transforms@7.29.7': - resolution: {integrity: sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==} - engines: {node: '>=6.9.0'} + "@babel/core": ^7.0.0 + + "@babel/helper-globals@7.29.7": + resolution: + { + integrity: sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==, + } + engines: { node: ">=6.9.0" } + + "@babel/helper-member-expression-to-functions@7.29.7": + resolution: + { + integrity: sha512-j+7JYmk1JYDtACIGj0QJqqWZjoUpMoEikQGADMaHgCMCSDqd2+P32rfcibUNrGOMWrlzK1WJBdxrB3JJQZwWtg==, + } + engines: { node: ">=6.9.0" } + + "@babel/helper-module-imports@7.29.7": + resolution: + { + integrity: sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==, + } + engines: { node: ">=6.9.0" } + + "@babel/helper-module-transforms@7.29.7": + resolution: + { + integrity: sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==, + } + engines: { node: ">=6.9.0" } peerDependencies: - '@babel/core': ^7.0.0 - - '@babel/helper-optimise-call-expression@7.29.7': - resolution: {integrity: sha512-+kmGVjcT9RGYzoDwdwEqEvGgKe3BYq+O1iGzjFubaNgZHwYHP6lsF2Yghf4kEuv9BV7tYDZ913aBW9am6YKong==} - engines: {node: '>=6.9.0'} - - '@babel/helper-plugin-utils@7.29.7': - resolution: {integrity: sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==} - engines: {node: '>=6.9.0'} - - '@babel/helper-replace-supers@7.29.7': - resolution: {integrity: sha512-atfGXWSeCiF4DnKZIfmJfQRkSw9b9gNNXR1kqKjbhG4pGYCOnkp8OcTB8E3NXjBu8NpheSnOeNKz8KT7UNFTmQ==} - engines: {node: '>=6.9.0'} + "@babel/core": ^7.0.0 + + "@babel/helper-optimise-call-expression@7.29.7": + resolution: + { + integrity: sha512-+kmGVjcT9RGYzoDwdwEqEvGgKe3BYq+O1iGzjFubaNgZHwYHP6lsF2Yghf4kEuv9BV7tYDZ913aBW9am6YKong==, + } + engines: { node: ">=6.9.0" } + + "@babel/helper-plugin-utils@7.29.7": + resolution: + { + integrity: sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==, + } + engines: { node: ">=6.9.0" } + + "@babel/helper-replace-supers@7.29.7": + resolution: + { + integrity: sha512-atfGXWSeCiF4DnKZIfmJfQRkSw9b9gNNXR1kqKjbhG4pGYCOnkp8OcTB8E3NXjBu8NpheSnOeNKz8KT7UNFTmQ==, + } + engines: { node: ">=6.9.0" } peerDependencies: - '@babel/core': ^7.0.0 - - '@babel/helper-skip-transparent-expression-wrappers@7.29.7': - resolution: {integrity: sha512-brcMGQaVzIeUb+6/bs1Av0f8YuNNjKY2JyvfRCsFuFsdKccEQ5Ges2y74D74NZ1Rz8lKJ9ksJkfqwQFJ/iNEyQ==} - engines: {node: '>=6.9.0'} - - '@babel/helper-string-parser@7.29.7': - resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} - engines: {node: '>=6.9.0'} - - '@babel/helper-string-parser@8.0.0': - resolution: {integrity: sha512-6mJgmFFFIIO82vvoLt9XtRC7/TkzXfts1t/SpRX4IHSzMgqoPYCWesVu1udUPUWioAE/2fcG6WuI8zrkE1gwrg==} - engines: {node: ^22.18.0 || >=24.11.0} - - '@babel/helper-validator-identifier@7.29.7': - resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} - engines: {node: '>=6.9.0'} - - '@babel/helper-validator-identifier@8.0.4': - resolution: {integrity: sha512-4wFaiLd0bVo4cIoTXI3zKI038NIWE/cr3jvBjejOVYVxV/m8Ltav1USiGzG1fmS5J2RhgEOgXNNK46cRPnRsrg==} - engines: {node: ^22.18.0 || >=24.11.0} - - '@babel/helper-validator-option@7.29.7': - resolution: {integrity: sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==} - engines: {node: '>=6.9.0'} - - '@babel/helpers@7.29.7': - resolution: {integrity: sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==} - engines: {node: '>=6.9.0'} - - '@babel/parser@7.29.7': - resolution: {integrity: sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==} - engines: {node: '>=6.0.0'} + "@babel/core": ^7.0.0 + + "@babel/helper-skip-transparent-expression-wrappers@7.29.7": + resolution: + { + integrity: sha512-brcMGQaVzIeUb+6/bs1Av0f8YuNNjKY2JyvfRCsFuFsdKccEQ5Ges2y74D74NZ1Rz8lKJ9ksJkfqwQFJ/iNEyQ==, + } + engines: { node: ">=6.9.0" } + + "@babel/helper-string-parser@7.29.7": + resolution: + { + integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==, + } + engines: { node: ">=6.9.0" } + + "@babel/helper-string-parser@8.0.0": + resolution: + { + integrity: sha512-6mJgmFFFIIO82vvoLt9XtRC7/TkzXfts1t/SpRX4IHSzMgqoPYCWesVu1udUPUWioAE/2fcG6WuI8zrkE1gwrg==, + } + engines: { node: ^22.18.0 || >=24.11.0 } + + "@babel/helper-validator-identifier@7.29.7": + resolution: + { + integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==, + } + engines: { node: ">=6.9.0" } + + "@babel/helper-validator-identifier@8.0.4": + resolution: + { + integrity: sha512-4wFaiLd0bVo4cIoTXI3zKI038NIWE/cr3jvBjejOVYVxV/m8Ltav1USiGzG1fmS5J2RhgEOgXNNK46cRPnRsrg==, + } + engines: { node: ^22.18.0 || >=24.11.0 } + + "@babel/helper-validator-option@7.29.7": + resolution: + { + integrity: sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==, + } + engines: { node: ">=6.9.0" } + + "@babel/helpers@7.29.7": + resolution: + { + integrity: sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==, + } + engines: { node: ">=6.9.0" } + + "@babel/parser@7.29.7": + resolution: + { + integrity: sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==, + } + engines: { node: ">=6.0.0" } hasBin: true - '@babel/parser@7.29.8': - resolution: {integrity: sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==} - engines: {node: '>=6.0.0'} + "@babel/parser@7.29.8": + resolution: + { + integrity: sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==, + } + engines: { node: ">=6.0.0" } hasBin: true - '@babel/parser@8.0.4': - resolution: {integrity: sha512-srpptsAkEbbNIC/q8nT7o+m6CQe8CJUTV/t7MYc9NnWlgYVtHOb7JH6SorxMhN0kuRJjVqXbKClG6xSbPtzz+g==} - engines: {node: ^22.18.0 || >=24.11.0} + "@babel/parser@8.0.4": + resolution: + { + integrity: sha512-srpptsAkEbbNIC/q8nT7o+m6CQe8CJUTV/t7MYc9NnWlgYVtHOb7JH6SorxMhN0kuRJjVqXbKClG6xSbPtzz+g==, + } + engines: { node: ^22.18.0 || >=24.11.0 } hasBin: true - '@babel/plugin-syntax-jsx@7.29.7': - resolution: {integrity: sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A==} - engines: {node: '>=6.9.0'} + "@babel/plugin-syntax-jsx@7.29.7": + resolution: + { + integrity: sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A==, + } + engines: { node: ">=6.9.0" } peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-typescript@7.29.7': - resolution: {integrity: sha512-ngr+82Sh0xMz25TPCZi+nC2iTzjfCdWS2ONXTp/PtSCHCgaCNBpdMqgvJ2ccdLlClVZ7sisIgB914j/JFe+RZA==} - engines: {node: '>=6.9.0'} + "@babel/core": ^7.0.0-0 + + "@babel/plugin-syntax-typescript@7.29.7": + resolution: + { + integrity: sha512-ngr+82Sh0xMz25TPCZi+nC2iTzjfCdWS2ONXTp/PtSCHCgaCNBpdMqgvJ2ccdLlClVZ7sisIgB914j/JFe+RZA==, + } + engines: { node: ">=6.9.0" } peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-typescript@7.29.7': - resolution: {integrity: sha512-jK52h8LaLc7JarhQV2ofeFMts4H7vnOXnqZNA6fYglBTZewRBE51KWt3BUltW1P+KoPsYkHoJeXePuz4zo2LMw==} - engines: {node: '>=6.9.0'} + "@babel/core": ^7.0.0-0 + + "@babel/plugin-transform-typescript@7.29.7": + resolution: + { + integrity: sha512-jK52h8LaLc7JarhQV2ofeFMts4H7vnOXnqZNA6fYglBTZewRBE51KWt3BUltW1P+KoPsYkHoJeXePuz4zo2LMw==, + } + engines: { node: ">=6.9.0" } peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/template@7.29.7': - resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==} - engines: {node: '>=6.9.0'} - - '@babel/traverse@7.29.8': - resolution: {integrity: sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==} - engines: {node: '>=6.9.0'} - - '@babel/types@7.29.7': - resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==} - engines: {node: '>=6.9.0'} - - '@babel/types@7.29.8': - resolution: {integrity: sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==} - engines: {node: '>=6.9.0'} - - '@babel/types@8.0.4': - resolution: {integrity: sha512-eY+Yn3dCqTGmyiq2QRU66lA5FL8lqqqvecHt0fF3uHONIa7ToYsaCiWV8lOKqAs0Rb2SjixiKFROngnulPtt2g==} - engines: {node: ^22.18.0 || >=24.11.0} - - '@bomb.sh/tab@0.0.19': - resolution: {integrity: sha512-dTRfo9Q9B+lbLG3JCu8a/AGQSfD2XXcFcnakQzVjSOX+VvR/s9zpsH8TlqV3iHqazniRn1Ypwd1hcRlXcu/4BA==} + "@babel/core": ^7.0.0-0 + + "@babel/template@7.29.7": + resolution: + { + integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==, + } + engines: { node: ">=6.9.0" } + + "@babel/traverse@7.29.8": + resolution: + { + integrity: sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==, + } + engines: { node: ">=6.9.0" } + + "@babel/types@7.29.7": + resolution: + { + integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==, + } + engines: { node: ">=6.9.0" } + + "@babel/types@7.29.8": + resolution: + { + integrity: sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==, + } + engines: { node: ">=6.9.0" } + + "@babel/types@8.0.4": + resolution: + { + integrity: sha512-eY+Yn3dCqTGmyiq2QRU66lA5FL8lqqqvecHt0fF3uHONIa7ToYsaCiWV8lOKqAs0Rb2SjixiKFROngnulPtt2g==, + } + engines: { node: ^22.18.0 || >=24.11.0 } + + "@bomb.sh/tab@0.0.19": + resolution: + { + integrity: sha512-dTRfo9Q9B+lbLG3JCu8a/AGQSfD2XXcFcnakQzVjSOX+VvR/s9zpsH8TlqV3iHqazniRn1Ypwd1hcRlXcu/4BA==, + } hasBin: true peerDependencies: cac: ^6.7.14 @@ -264,870 +367,1344 @@ packages: commander: optional: true - '@bufbuild/protobuf@2.10.1': - resolution: {integrity: sha512-ckS3+vyJb5qGpEYv/s1OebUHDi/xSNtfgw1wqKZo7MR9F2z+qXr0q5XagafAG/9O0QPVIUfST0smluYSTpYFkg==} - - '@clack/core@1.4.3': - resolution: {integrity: sha512-/kr3UWNtdJfxZtPgDqUOmG2pvwlmcLGheex5yiZKdwbzZJxhV+HMNR9QNmyY5cGwTNV6LrR7Jtp+KjhUAP1qBQ==} - engines: {node: '>= 20.12.0'} - - '@clack/prompts@1.7.0': - resolution: {integrity: sha512-y7/yvZ2TPAnR9+jnc00klvNNLkJiXFFrQA/hlLCcxA9a2A4zQIOimyFQ9XfwYKiGD1fb5GY8vbKIIgO8d5Tb2A==} - engines: {node: '>= 20.12.0'} - - '@cloudflare/kv-asset-handler@0.4.2': - resolution: {integrity: sha512-SIOD2DxrRRwQ+jgzlXCqoEFiKOFqaPjhnNTGKXSRLvp1HiOvapLaFG2kEr9dYQTYe8rKrd9uvDUzmAITeNyaHQ==} - engines: {node: '>=18.0.0'} - - '@colordx/core@5.5.0': - resolution: {integrity: sha512-3PxTH8itZzltK0U9jTwVVnjLXvnDYuq3m+QXsHkENxWiPRh4WaoLcs1SQjqgZ55kS+QyirpH5BVwzP2gMVG6EQ==} - - '@csstools/selector-resolve-nested@3.1.0': - resolution: {integrity: sha512-mf1LEW0tJLKfWyvn5KdDrhpxHyuxpbNwTIwOYLIvsTffeyOf85j5oIzfG0yosxDgx/sswlqBnESYUcQH0vgZ0g==} - engines: {node: '>=18'} + "@bufbuild/protobuf@2.10.1": + resolution: + { + integrity: sha512-ckS3+vyJb5qGpEYv/s1OebUHDi/xSNtfgw1wqKZo7MR9F2z+qXr0q5XagafAG/9O0QPVIUfST0smluYSTpYFkg==, + } + + "@clack/core@1.4.3": + resolution: + { + integrity: sha512-/kr3UWNtdJfxZtPgDqUOmG2pvwlmcLGheex5yiZKdwbzZJxhV+HMNR9QNmyY5cGwTNV6LrR7Jtp+KjhUAP1qBQ==, + } + engines: { node: ">= 20.12.0" } + + "@clack/prompts@1.7.0": + resolution: + { + integrity: sha512-y7/yvZ2TPAnR9+jnc00klvNNLkJiXFFrQA/hlLCcxA9a2A4zQIOimyFQ9XfwYKiGD1fb5GY8vbKIIgO8d5Tb2A==, + } + engines: { node: ">= 20.12.0" } + + "@cloudflare/kv-asset-handler@0.4.2": + resolution: + { + integrity: sha512-SIOD2DxrRRwQ+jgzlXCqoEFiKOFqaPjhnNTGKXSRLvp1HiOvapLaFG2kEr9dYQTYe8rKrd9uvDUzmAITeNyaHQ==, + } + engines: { node: ">=18.0.0" } + + "@colordx/core@5.5.0": + resolution: + { + integrity: sha512-3PxTH8itZzltK0U9jTwVVnjLXvnDYuq3m+QXsHkENxWiPRh4WaoLcs1SQjqgZ55kS+QyirpH5BVwzP2gMVG6EQ==, + } + + "@csstools/selector-resolve-nested@3.1.0": + resolution: + { + integrity: sha512-mf1LEW0tJLKfWyvn5KdDrhpxHyuxpbNwTIwOYLIvsTffeyOf85j5oIzfG0yosxDgx/sswlqBnESYUcQH0vgZ0g==, + } + engines: { node: ">=18" } peerDependencies: postcss-selector-parser: ^7.0.0 - '@csstools/selector-specificity@5.0.0': - resolution: {integrity: sha512-PCqQV3c4CoVm3kdPhyeZ07VmBRdH2EpMFA/pd9OASpOEC3aXNGoqPDAZ80D0cLpMBxnmk0+yNhGsEx31hq7Gtw==} - engines: {node: '>=18'} + "@csstools/selector-specificity@5.0.0": + resolution: + { + integrity: sha512-PCqQV3c4CoVm3kdPhyeZ07VmBRdH2EpMFA/pd9OASpOEC3aXNGoqPDAZ80D0cLpMBxnmk0+yNhGsEx31hq7Gtw==, + } + engines: { node: ">=18" } peerDependencies: postcss-selector-parser: ^7.0.0 - '@dxup/nuxt@0.5.6': - resolution: {integrity: sha512-uZjAFoocWtWHr7YP9jm6FqwI8kjX2QN9bjcncoZt0GS+zExIAP3Ewv6EqEUvVP7w9pjZE+T19QxLHeoacN/MNg==} - - '@dxup/unimport@0.1.2': - resolution: {integrity: sha512-/B8YJGPzaYq1NbsQmwgP8EZqg40NpTw4ZB3suuI0TplbxKHeK94jeaawLmVhCv+YwUnOpiWEz9U6SeThku/8JQ==} - - '@esbuild/aix-ppc64@0.28.1': - resolution: {integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==} - engines: {node: '>=18'} + "@dxup/nuxt@0.5.6": + resolution: + { + integrity: sha512-uZjAFoocWtWHr7YP9jm6FqwI8kjX2QN9bjcncoZt0GS+zExIAP3Ewv6EqEUvVP7w9pjZE+T19QxLHeoacN/MNg==, + } + + "@dxup/unimport@0.1.2": + resolution: + { + integrity: sha512-/B8YJGPzaYq1NbsQmwgP8EZqg40NpTw4ZB3suuI0TplbxKHeK94jeaawLmVhCv+YwUnOpiWEz9U6SeThku/8JQ==, + } + + "@esbuild/aix-ppc64@0.28.1": + resolution: + { + integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==, + } + engines: { node: ">=18" } cpu: [ppc64] os: [aix] - '@esbuild/android-arm64@0.28.1': - resolution: {integrity: sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==} - engines: {node: '>=18'} + "@esbuild/android-arm64@0.28.1": + resolution: + { + integrity: sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==, + } + engines: { node: ">=18" } cpu: [arm64] os: [android] - '@esbuild/android-arm@0.28.1': - resolution: {integrity: sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==} - engines: {node: '>=18'} + "@esbuild/android-arm@0.28.1": + resolution: + { + integrity: sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==, + } + engines: { node: ">=18" } cpu: [arm] os: [android] - '@esbuild/android-x64@0.28.1': - resolution: {integrity: sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==} - engines: {node: '>=18'} + "@esbuild/android-x64@0.28.1": + resolution: + { + integrity: sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==, + } + engines: { node: ">=18" } cpu: [x64] os: [android] - '@esbuild/darwin-arm64@0.28.1': - resolution: {integrity: sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==} - engines: {node: '>=18'} + "@esbuild/darwin-arm64@0.28.1": + resolution: + { + integrity: sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==, + } + engines: { node: ">=18" } cpu: [arm64] os: [darwin] - '@esbuild/darwin-x64@0.28.1': - resolution: {integrity: sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==} - engines: {node: '>=18'} + "@esbuild/darwin-x64@0.28.1": + resolution: + { + integrity: sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==, + } + engines: { node: ">=18" } cpu: [x64] os: [darwin] - '@esbuild/freebsd-arm64@0.28.1': - resolution: {integrity: sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==} - engines: {node: '>=18'} + "@esbuild/freebsd-arm64@0.28.1": + resolution: + { + integrity: sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==, + } + engines: { node: ">=18" } cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-x64@0.28.1': - resolution: {integrity: sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==} - engines: {node: '>=18'} + "@esbuild/freebsd-x64@0.28.1": + resolution: + { + integrity: sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==, + } + engines: { node: ">=18" } cpu: [x64] os: [freebsd] - '@esbuild/linux-arm64@0.28.1': - resolution: {integrity: sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==} - engines: {node: '>=18'} + "@esbuild/linux-arm64@0.28.1": + resolution: + { + integrity: sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==, + } + engines: { node: ">=18" } cpu: [arm64] os: [linux] - '@esbuild/linux-arm@0.28.1': - resolution: {integrity: sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==} - engines: {node: '>=18'} + "@esbuild/linux-arm@0.28.1": + resolution: + { + integrity: sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==, + } + engines: { node: ">=18" } cpu: [arm] os: [linux] - '@esbuild/linux-ia32@0.28.1': - resolution: {integrity: sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==} - engines: {node: '>=18'} + "@esbuild/linux-ia32@0.28.1": + resolution: + { + integrity: sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==, + } + engines: { node: ">=18" } cpu: [ia32] os: [linux] - '@esbuild/linux-loong64@0.28.1': - resolution: {integrity: sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==} - engines: {node: '>=18'} + "@esbuild/linux-loong64@0.28.1": + resolution: + { + integrity: sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==, + } + engines: { node: ">=18" } cpu: [loong64] os: [linux] - '@esbuild/linux-mips64el@0.28.1': - resolution: {integrity: sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==} - engines: {node: '>=18'} + "@esbuild/linux-mips64el@0.28.1": + resolution: + { + integrity: sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==, + } + engines: { node: ">=18" } cpu: [mips64el] os: [linux] - '@esbuild/linux-ppc64@0.28.1': - resolution: {integrity: sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==} - engines: {node: '>=18'} + "@esbuild/linux-ppc64@0.28.1": + resolution: + { + integrity: sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==, + } + engines: { node: ">=18" } cpu: [ppc64] os: [linux] - '@esbuild/linux-riscv64@0.28.1': - resolution: {integrity: sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==} - engines: {node: '>=18'} + "@esbuild/linux-riscv64@0.28.1": + resolution: + { + integrity: sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==, + } + engines: { node: ">=18" } cpu: [riscv64] os: [linux] - '@esbuild/linux-s390x@0.28.1': - resolution: {integrity: sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==} - engines: {node: '>=18'} + "@esbuild/linux-s390x@0.28.1": + resolution: + { + integrity: sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==, + } + engines: { node: ">=18" } cpu: [s390x] os: [linux] - '@esbuild/linux-x64@0.28.1': - resolution: {integrity: sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==} - engines: {node: '>=18'} + "@esbuild/linux-x64@0.28.1": + resolution: + { + integrity: sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==, + } + engines: { node: ">=18" } cpu: [x64] os: [linux] - '@esbuild/netbsd-arm64@0.28.1': - resolution: {integrity: sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==} - engines: {node: '>=18'} + "@esbuild/netbsd-arm64@0.28.1": + resolution: + { + integrity: sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==, + } + engines: { node: ">=18" } cpu: [arm64] os: [netbsd] - '@esbuild/netbsd-x64@0.28.1': - resolution: {integrity: sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==} - engines: {node: '>=18'} + "@esbuild/netbsd-x64@0.28.1": + resolution: + { + integrity: sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==, + } + engines: { node: ">=18" } cpu: [x64] os: [netbsd] - '@esbuild/openbsd-arm64@0.28.1': - resolution: {integrity: sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==} - engines: {node: '>=18'} + "@esbuild/openbsd-arm64@0.28.1": + resolution: + { + integrity: sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==, + } + engines: { node: ">=18" } cpu: [arm64] os: [openbsd] - '@esbuild/openbsd-x64@0.28.1': - resolution: {integrity: sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==} - engines: {node: '>=18'} + "@esbuild/openbsd-x64@0.28.1": + resolution: + { + integrity: sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==, + } + engines: { node: ">=18" } cpu: [x64] os: [openbsd] - '@esbuild/openharmony-arm64@0.28.1': - resolution: {integrity: sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==} - engines: {node: '>=18'} + "@esbuild/openharmony-arm64@0.28.1": + resolution: + { + integrity: sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==, + } + engines: { node: ">=18" } cpu: [arm64] os: [openharmony] - '@esbuild/sunos-x64@0.28.1': - resolution: {integrity: sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==} - engines: {node: '>=18'} + "@esbuild/sunos-x64@0.28.1": + resolution: + { + integrity: sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==, + } + engines: { node: ">=18" } cpu: [x64] os: [sunos] - '@esbuild/win32-arm64@0.28.1': - resolution: {integrity: sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==} - engines: {node: '>=18'} + "@esbuild/win32-arm64@0.28.1": + resolution: + { + integrity: sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==, + } + engines: { node: ">=18" } cpu: [arm64] os: [win32] - '@esbuild/win32-ia32@0.28.1': - resolution: {integrity: sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==} - engines: {node: '>=18'} + "@esbuild/win32-ia32@0.28.1": + resolution: + { + integrity: sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==, + } + engines: { node: ">=18" } cpu: [ia32] os: [win32] - '@esbuild/win32-x64@0.28.1': - resolution: {integrity: sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==} - engines: {node: '>=18'} + "@esbuild/win32-x64@0.28.1": + resolution: + { + integrity: sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==, + } + engines: { node: ">=18" } cpu: [x64] os: [win32] - '@headlessui/vue@1.7.23': - resolution: {integrity: sha512-JzdCNqurrtuu0YW6QaDtR2PIYCKPUWq28csDyMvN4zmGccmE7lz40Is6hc3LA4HFeCI7sekZ/PQMTNmn9I/4Wg==} - engines: {node: '>=10'} + "@headlessui/vue@1.7.23": + resolution: + { + integrity: sha512-JzdCNqurrtuu0YW6QaDtR2PIYCKPUWq28csDyMvN4zmGccmE7lz40Is6hc3LA4HFeCI7sekZ/PQMTNmn9I/4Wg==, + } + engines: { node: ">=10" } peerDependencies: vue: ^3.2.0 - '@heroicons/vue@2.2.0': - resolution: {integrity: sha512-G3dbSxoeEKqbi/DFalhRxJU4mTXJn7GwZ7ae8NuEQzd1bqdd0jAbdaBZlHPcvPD2xI1iGzNVB4k20Un2AguYPw==} + "@heroicons/vue@2.2.0": + resolution: + { + integrity: sha512-G3dbSxoeEKqbi/DFalhRxJU4mTXJn7GwZ7ae8NuEQzd1bqdd0jAbdaBZlHPcvPD2xI1iGzNVB4k20Un2AguYPw==, + } peerDependencies: - vue: '>= 3' - - '@ioredis/commands@1.10.0': - resolution: {integrity: sha512-UmeW7z4LfctwoQ5wkhVzgq8tXkreED2xZGpX+Bg+zA+WJFZCT6c062AfCK/Dfk81xZnnwdhJCUMkitihRaoC2Q==} - - '@isaacs/cliui@8.0.2': - resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} - engines: {node: '>=12'} - - '@isaacs/fs-minipass@4.0.1': - resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} - engines: {node: '>=18.0.0'} - - '@jridgewell/gen-mapping@0.3.13': - resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} - - '@jridgewell/remapping@2.3.5': - resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} - - '@jridgewell/resolve-uri@3.1.2': - resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} - engines: {node: '>=6.0.0'} - - '@jridgewell/source-map@0.3.11': - resolution: {integrity: sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==} - - '@jridgewell/sourcemap-codec@1.5.5': - resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} - - '@jridgewell/trace-mapping@0.3.31': - resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} - - '@koa/router@12.0.2': - resolution: {integrity: sha512-sYcHglGKTxGF+hQ6x67xDfkE9o+NhVlRHBqq6gLywaMc6CojK/5vFZByphdonKinYlMLkEkacm+HEse9HzwgTA==} - engines: {node: '>= 12'} + vue: ">= 3" + + "@ioredis/commands@1.10.0": + resolution: + { + integrity: sha512-UmeW7z4LfctwoQ5wkhVzgq8tXkreED2xZGpX+Bg+zA+WJFZCT6c062AfCK/Dfk81xZnnwdhJCUMkitihRaoC2Q==, + } + + "@isaacs/cliui@8.0.2": + resolution: + { + integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==, + } + engines: { node: ">=12" } + + "@isaacs/fs-minipass@4.0.1": + resolution: + { + integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==, + } + engines: { node: ">=18.0.0" } + + "@jridgewell/gen-mapping@0.3.13": + resolution: + { + integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==, + } + + "@jridgewell/remapping@2.3.5": + resolution: + { + integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==, + } + + "@jridgewell/resolve-uri@3.1.2": + resolution: + { + integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==, + } + engines: { node: ">=6.0.0" } + + "@jridgewell/source-map@0.3.11": + resolution: + { + integrity: sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==, + } + + "@jridgewell/sourcemap-codec@1.5.5": + resolution: + { + integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==, + } + + "@jridgewell/trace-mapping@0.3.31": + resolution: + { + integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==, + } + + "@koa/router@12.0.2": + resolution: + { + integrity: sha512-sYcHglGKTxGF+hQ6x67xDfkE9o+NhVlRHBqq6gLywaMc6CojK/5vFZByphdonKinYlMLkEkacm+HEse9HzwgTA==, + } + engines: { node: ">= 12" } deprecated: Please upgrade to v15 or higher. All reported bugs in this version are fixed in newer releases, dependencies have been updated, and security has been improved. - '@kwsites/file-exists@1.1.1': - resolution: {integrity: sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==} - - '@kwsites/promise-deferred@1.1.1': - resolution: {integrity: sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==} - - '@mapbox/node-pre-gyp@2.0.3': - resolution: {integrity: sha512-uwPAhccfFJlsfCxMYTwOdVfOz3xqyj8xYL3zJj8f0pb30tLohnnFPhLuqp4/qoEz8sNxe4SESZedcBojRefIzg==} - engines: {node: '>=18'} + "@kwsites/file-exists@1.1.1": + resolution: + { + integrity: sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==, + } + + "@kwsites/promise-deferred@1.1.1": + resolution: + { + integrity: sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==, + } + + "@mapbox/node-pre-gyp@2.0.3": + resolution: + { + integrity: sha512-uwPAhccfFJlsfCxMYTwOdVfOz3xqyj8xYL3zJj8f0pb30tLohnnFPhLuqp4/qoEz8sNxe4SESZedcBojRefIzg==, + } + engines: { node: ">=18" } hasBin: true - '@napi-rs/lzma-linux-x64-gnu@1.5.1': - resolution: {integrity: sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==} - engines: {node: ^22.20 || ^24.12 || >=25} + "@napi-rs/lzma-linux-x64-gnu@1.5.1": + resolution: + { + integrity: sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==, + } + engines: { node: ^22.20 || ^24.12 || >=25 } cpu: [x64] os: [linux] libc: [glibc] - '@nodelib/fs.scandir@2.1.5': - resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} - engines: {node: '>= 8'} - - '@nodelib/fs.stat@2.0.5': - resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} - engines: {node: '>= 8'} - - '@nodelib/fs.walk@1.2.8': - resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} - engines: {node: '>= 8'} - - '@nuxt/cli@3.37.0': - resolution: {integrity: sha512-Zj9NwHjEBzVrgezsgMFjpMhNqwNgROk9DzNi/dyfk1mCbXIRigV11br2xOl0Satek30bmv7oZAfMtCnDe6Ip0Q==} - engines: {node: ^16.14.0 || >=18.0.0} + "@nodelib/fs.scandir@2.1.5": + resolution: + { + integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==, + } + engines: { node: ">= 8" } + + "@nodelib/fs.stat@2.0.5": + resolution: + { + integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==, + } + engines: { node: ">= 8" } + + "@nodelib/fs.walk@1.2.8": + resolution: + { + integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==, + } + engines: { node: ">= 8" } + + "@nuxt/cli@3.37.0": + resolution: + { + integrity: sha512-Zj9NwHjEBzVrgezsgMFjpMhNqwNgROk9DzNi/dyfk1mCbXIRigV11br2xOl0Satek30bmv7oZAfMtCnDe6Ip0Q==, + } + engines: { node: ^16.14.0 || >=18.0.0 } hasBin: true peerDependencies: - '@nuxt/schema': ^4.4.6 + "@nuxt/schema": ^4.4.6 peerDependenciesMeta: - '@nuxt/schema': + "@nuxt/schema": optional: true - '@nuxt/devalue@2.0.2': - resolution: {integrity: sha512-GBzP8zOc7CGWyFQS6dv1lQz8VVpz5C2yRszbXufwG/9zhStTIH50EtD87NmWbTMwXDvZLNg8GIpb1UFdH93JCA==} + "@nuxt/devalue@2.0.2": + resolution: + { + integrity: sha512-GBzP8zOc7CGWyFQS6dv1lQz8VVpz5C2yRszbXufwG/9zhStTIH50EtD87NmWbTMwXDvZLNg8GIpb1UFdH93JCA==, + } - '@nuxt/devtools-kit@3.4.1': - resolution: {integrity: sha512-6ltPm2yUW8GvDdf3VJna7+flLi+ej7xRfSr+S4ovevKt/CuPf9EqYOn1jW7Kw/U1MXt/71zkn+/O2vu3G6O9Hg==} + "@nuxt/devtools-kit@3.4.1": + resolution: + { + integrity: sha512-6ltPm2yUW8GvDdf3VJna7+flLi+ej7xRfSr+S4ovevKt/CuPf9EqYOn1jW7Kw/U1MXt/71zkn+/O2vu3G6O9Hg==, + } peerDependencies: - vite: '>=6.0' + vite: ">=6.0" - '@nuxt/devtools-wizard@3.4.1': - resolution: {integrity: sha512-taGeuTnkHsZVjgD9r6NXvvHaBzB2j4mCgOmlmkz3ntsqgh1SJfmsD11Tmtl7FVAxJS8Wuvuzgt0GyTlADBW9Wg==} + "@nuxt/devtools-wizard@3.4.1": + resolution: + { + integrity: sha512-taGeuTnkHsZVjgD9r6NXvvHaBzB2j4mCgOmlmkz3ntsqgh1SJfmsD11Tmtl7FVAxJS8Wuvuzgt0GyTlADBW9Wg==, + } hasBin: true - '@nuxt/devtools@3.4.1': - resolution: {integrity: sha512-20zZs6k/zAdi+PM7s1BwxE3hanZ+R1OGi5DWCKPyzSnhUUeeNebvWNgec7Rwnx6iKLkJfK4WFIZ+FL2QurG/ZQ==} + "@nuxt/devtools@3.4.1": + resolution: + { + integrity: sha512-20zZs6k/zAdi+PM7s1BwxE3hanZ+R1OGi5DWCKPyzSnhUUeeNebvWNgec7Rwnx6iKLkJfK4WFIZ+FL2QurG/ZQ==, + } hasBin: true peerDependencies: - '@vitejs/devtools': '*' - vite: '>=6.0' + "@vitejs/devtools": "*" + vite: ">=6.0" peerDependenciesMeta: - '@vitejs/devtools': - optional: true - - '@nuxt/kit@3.20.1': - resolution: {integrity: sha512-TIslaylfI5kd3AxX5qts0qyrIQ9Uq3HAA1bgIIJ+c+zpDfK338YS+YrCWxBBzDMECRCbAS58mqAd2MtJfG1ENA==} - engines: {node: '>=18.12.0'} - - '@nuxt/kit@4.5.1': - resolution: {integrity: sha512-xDXQspE2blxaZwxwGknL/BqaIbkLizl85Ov+pbSlPPd/rw2KjJGx88RHU5SwoQNwCiSC5hWZBnVAznIsq1xNHQ==} - engines: {node: '>=18.12.0'} - - '@nuxt/nitro-server@4.5.1': - resolution: {integrity: sha512-bYg+Rri9qLNiNnK70mvdghwwWtOEvlvBn3BnnEZWEFM8A51zM803Ltg+TwR6B6/1jdYOdZ6cnso4pvpCJuPlww==} - engines: {node: ^22.19.0 || ^24.11.0 || >=26.0.0} + "@vitejs/devtools": + optional: true + + "@nuxt/kit@3.20.1": + resolution: + { + integrity: sha512-TIslaylfI5kd3AxX5qts0qyrIQ9Uq3HAA1bgIIJ+c+zpDfK338YS+YrCWxBBzDMECRCbAS58mqAd2MtJfG1ENA==, + } + engines: { node: ">=18.12.0" } + + "@nuxt/kit@4.5.1": + resolution: + { + integrity: sha512-xDXQspE2blxaZwxwGknL/BqaIbkLizl85Ov+pbSlPPd/rw2KjJGx88RHU5SwoQNwCiSC5hWZBnVAznIsq1xNHQ==, + } + engines: { node: ">=18.12.0" } + + "@nuxt/nitro-server@4.5.1": + resolution: + { + integrity: sha512-bYg+Rri9qLNiNnK70mvdghwwWtOEvlvBn3BnnEZWEFM8A51zM803Ltg+TwR6B6/1jdYOdZ6cnso4pvpCJuPlww==, + } + engines: { node: ^22.19.0 || ^24.11.0 || >=26.0.0 } peerDependencies: - '@babel/plugin-proposal-decorators': ^7.25.0 || ^8.0.0 - '@babel/plugin-syntax-typescript': ^7.25.0 || ^8.0.0 - '@rollup/plugin-babel': ^6.0.0 || ^7.0.0 + "@babel/plugin-proposal-decorators": ^7.25.0 || ^8.0.0 + "@babel/plugin-syntax-typescript": ^7.25.0 || ^8.0.0 + "@rollup/plugin-babel": ^6.0.0 || ^7.0.0 nuxt: ^4.5.1 peerDependenciesMeta: - '@babel/plugin-proposal-decorators': + "@babel/plugin-proposal-decorators": optional: true - '@babel/plugin-syntax-typescript': + "@babel/plugin-syntax-typescript": optional: true - '@rollup/plugin-babel': + "@rollup/plugin-babel": optional: true - '@nuxt/schema@4.5.1': - resolution: {integrity: sha512-RDdu0BBg9y+Eiyu95LUDJ57YrejJ5v1/VA2r5oBLYiiSiLrlVt33HN9ut4qSyUN8gRTOXnFADery8AyJpTjdWA==} - engines: {node: ^14.18.0 || >=16.10.0} + "@nuxt/schema@4.5.1": + resolution: + { + integrity: sha512-RDdu0BBg9y+Eiyu95LUDJ57YrejJ5v1/VA2r5oBLYiiSiLrlVt33HN9ut4qSyUN8gRTOXnFADery8AyJpTjdWA==, + } + engines: { node: ^14.18.0 || >=16.10.0 } - '@nuxt/telemetry@2.8.0': - resolution: {integrity: sha512-zAwXY24KYvpLTmiV+osagd2EHkfs5IF+7oDZYTQoit5r0kPlwaCNlzHp5I/wUAWT4LBw6lG8gZ6bWidAdv/erQ==} - engines: {node: '>=18.12.0'} + "@nuxt/telemetry@2.8.0": + resolution: + { + integrity: sha512-zAwXY24KYvpLTmiV+osagd2EHkfs5IF+7oDZYTQoit5r0kPlwaCNlzHp5I/wUAWT4LBw6lG8gZ6bWidAdv/erQ==, + } + engines: { node: ">=18.12.0" } hasBin: true peerDependencies: - '@nuxt/kit': '>=3.0.0' - - '@nuxt/vite-builder@4.5.1': - resolution: {integrity: sha512-gHMqCZ4Fd9lvJ6FUh9qyzD5Sdx0UyM1pxV+wzjlQvBbX4NxvB16spMcNGUUnnV6nX2so3JJROXxghlKf7Uj6nw==} - engines: {node: ^22.18.0 || ^24.11.0 || >=26.0.0} + "@nuxt/kit": ">=3.0.0" + + "@nuxt/vite-builder@4.5.1": + resolution: + { + integrity: sha512-gHMqCZ4Fd9lvJ6FUh9qyzD5Sdx0UyM1pxV+wzjlQvBbX4NxvB16spMcNGUUnnV6nX2so3JJROXxghlKf7Uj6nw==, + } + engines: { node: ^22.18.0 || ^24.11.0 || >=26.0.0 } peerDependencies: - '@babel/plugin-proposal-decorators': ^7.25.0 || ^8.0.0 - '@babel/plugin-syntax-jsx': ^7.25.0 || ^8.0.0 + "@babel/plugin-proposal-decorators": ^7.25.0 || ^8.0.0 + "@babel/plugin-syntax-jsx": ^7.25.0 || ^8.0.0 nuxt: 4.5.1 rolldown: ^1.0.0 rollup-plugin-visualizer: ^6.0.0 || ^7.0.1 vue: ^3.3.4 peerDependenciesMeta: - '@babel/plugin-proposal-decorators': + "@babel/plugin-proposal-decorators": optional: true - '@babel/plugin-syntax-jsx': + "@babel/plugin-syntax-jsx": optional: true rolldown: optional: true rollup-plugin-visualizer: optional: true - '@nuxtjs/tailwindcss@6.14.0': - resolution: {integrity: sha512-30RyDK++LrUVRgc2A85MktGWIZoRQgeQKjE4CjjD64OXNozyl+4ScHnnYgqVToMM6Ch2ZG2W4wV2J0EN6F0zkQ==} - - '@oxc-project/types@0.143.0': - resolution: {integrity: sha512-u6JZdLBTLotrNC9Vd6vPssINdzcCzleKAH6EJKImQb7GtYvX5keN2dxkoK44stCc4tffE6QQRtZTXVSzsLUlWA==} - - '@parcel/watcher-android-arm64@2.5.6': - resolution: {integrity: sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==} - engines: {node: '>= 10.0.0'} + "@nuxtjs/tailwindcss@6.14.0": + resolution: + { + integrity: sha512-30RyDK++LrUVRgc2A85MktGWIZoRQgeQKjE4CjjD64OXNozyl+4ScHnnYgqVToMM6Ch2ZG2W4wV2J0EN6F0zkQ==, + } + + "@oxc-project/types@0.143.0": + resolution: + { + integrity: sha512-u6JZdLBTLotrNC9Vd6vPssINdzcCzleKAH6EJKImQb7GtYvX5keN2dxkoK44stCc4tffE6QQRtZTXVSzsLUlWA==, + } + + "@parcel/watcher-android-arm64@2.5.6": + resolution: + { + integrity: sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==, + } + engines: { node: ">= 10.0.0" } cpu: [arm64] os: [android] - '@parcel/watcher-darwin-arm64@2.5.6': - resolution: {integrity: sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==} - engines: {node: '>= 10.0.0'} + "@parcel/watcher-darwin-arm64@2.5.6": + resolution: + { + integrity: sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==, + } + engines: { node: ">= 10.0.0" } cpu: [arm64] os: [darwin] - '@parcel/watcher-darwin-x64@2.5.6': - resolution: {integrity: sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==} - engines: {node: '>= 10.0.0'} + "@parcel/watcher-darwin-x64@2.5.6": + resolution: + { + integrity: sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==, + } + engines: { node: ">= 10.0.0" } cpu: [x64] os: [darwin] - '@parcel/watcher-freebsd-x64@2.5.6': - resolution: {integrity: sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==} - engines: {node: '>= 10.0.0'} + "@parcel/watcher-freebsd-x64@2.5.6": + resolution: + { + integrity: sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==, + } + engines: { node: ">= 10.0.0" } cpu: [x64] os: [freebsd] - '@parcel/watcher-linux-arm-glibc@2.5.6': - resolution: {integrity: sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==} - engines: {node: '>= 10.0.0'} + "@parcel/watcher-linux-arm-glibc@2.5.6": + resolution: + { + integrity: sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==, + } + engines: { node: ">= 10.0.0" } cpu: [arm] os: [linux] libc: [glibc] - '@parcel/watcher-linux-arm-musl@2.5.6': - resolution: {integrity: sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==} - engines: {node: '>= 10.0.0'} + "@parcel/watcher-linux-arm-musl@2.5.6": + resolution: + { + integrity: sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==, + } + engines: { node: ">= 10.0.0" } cpu: [arm] os: [linux] libc: [musl] - '@parcel/watcher-linux-arm64-glibc@2.5.6': - resolution: {integrity: sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==} - engines: {node: '>= 10.0.0'} + "@parcel/watcher-linux-arm64-glibc@2.5.6": + resolution: + { + integrity: sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==, + } + engines: { node: ">= 10.0.0" } cpu: [arm64] os: [linux] libc: [glibc] - '@parcel/watcher-linux-arm64-musl@2.5.6': - resolution: {integrity: sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==} - engines: {node: '>= 10.0.0'} + "@parcel/watcher-linux-arm64-musl@2.5.6": + resolution: + { + integrity: sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==, + } + engines: { node: ">= 10.0.0" } cpu: [arm64] os: [linux] libc: [musl] - '@parcel/watcher-linux-x64-glibc@2.5.6': - resolution: {integrity: sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==} - engines: {node: '>= 10.0.0'} + "@parcel/watcher-linux-x64-glibc@2.5.6": + resolution: + { + integrity: sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==, + } + engines: { node: ">= 10.0.0" } cpu: [x64] os: [linux] libc: [glibc] - '@parcel/watcher-linux-x64-musl@2.5.6': - resolution: {integrity: sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==} - engines: {node: '>= 10.0.0'} + "@parcel/watcher-linux-x64-musl@2.5.6": + resolution: + { + integrity: sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==, + } + engines: { node: ">= 10.0.0" } cpu: [x64] os: [linux] libc: [musl] - '@parcel/watcher-wasm@2.6.0': - resolution: {integrity: sha512-dtjbDxKSDPQ8AmA+pS4OFaHE1FKrjtGpLGBxw85uKFkRorjNbvDM/aFPgqosu40wprbp1xw2ZSxIKqghCUHe2w==} - engines: {node: '>= 10.0.0'} + "@parcel/watcher-wasm@2.6.0": + resolution: + { + integrity: sha512-dtjbDxKSDPQ8AmA+pS4OFaHE1FKrjtGpLGBxw85uKFkRorjNbvDM/aFPgqosu40wprbp1xw2ZSxIKqghCUHe2w==, + } + engines: { node: ">= 10.0.0" } bundledDependencies: - napi-wasm - '@parcel/watcher-win32-arm64@2.5.6': - resolution: {integrity: sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==} - engines: {node: '>= 10.0.0'} + "@parcel/watcher-win32-arm64@2.5.6": + resolution: + { + integrity: sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==, + } + engines: { node: ">= 10.0.0" } cpu: [arm64] os: [win32] - '@parcel/watcher-win32-ia32@2.5.6': - resolution: {integrity: sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==} - engines: {node: '>= 10.0.0'} + "@parcel/watcher-win32-ia32@2.5.6": + resolution: + { + integrity: sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==, + } + engines: { node: ">= 10.0.0" } cpu: [ia32] os: [win32] - '@parcel/watcher-win32-x64@2.5.6': - resolution: {integrity: sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==} - engines: {node: '>= 10.0.0'} + "@parcel/watcher-win32-x64@2.5.6": + resolution: + { + integrity: sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==, + } + engines: { node: ">= 10.0.0" } cpu: [x64] os: [win32] - '@parcel/watcher@2.5.6': - resolution: {integrity: sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==} - engines: {node: '>= 10.0.0'} - - '@pkgjs/parseargs@0.11.0': - resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} - engines: {node: '>=14'} - - '@polka/url@1.0.0-next.29': - resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} - - '@poppinss/colors@4.1.6': - resolution: {integrity: sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==} - - '@poppinss/dumper@0.7.0': - resolution: {integrity: sha512-0UTYalzk2t6S4rA2uHOz5bSSW2CHdv4vggJI6Alg90yvl0UgXs6XSXpH96OH+bRkX4J/06djv29pqXJ0lq5Kag==} - - '@poppinss/exception@1.2.3': - resolution: {integrity: sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==} - - '@rolldown/binding-android-arm64@1.2.3': - resolution: {integrity: sha512-zrJtHDcaZJ1Fp7xf4hNl+7seH9Cn/N5TwLYkhgXREtBwAd/jaqW3uqeHxpDugJLVICWg4eW44kOQEGJ1r6jCGw==} - engines: {node: ^20.19.0 || >=22.12.0} + "@parcel/watcher@2.5.6": + resolution: + { + integrity: sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==, + } + engines: { node: ">= 10.0.0" } + + "@pkgjs/parseargs@0.11.0": + resolution: + { + integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==, + } + engines: { node: ">=14" } + + "@polka/url@1.0.0-next.29": + resolution: + { + integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==, + } + + "@poppinss/colors@4.1.6": + resolution: + { + integrity: sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==, + } + + "@poppinss/dumper@0.7.0": + resolution: + { + integrity: sha512-0UTYalzk2t6S4rA2uHOz5bSSW2CHdv4vggJI6Alg90yvl0UgXs6XSXpH96OH+bRkX4J/06djv29pqXJ0lq5Kag==, + } + + "@poppinss/exception@1.2.3": + resolution: + { + integrity: sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==, + } + + "@rolldown/binding-android-arm64@1.2.3": + resolution: + { + integrity: sha512-zrJtHDcaZJ1Fp7xf4hNl+7seH9Cn/N5TwLYkhgXREtBwAd/jaqW3uqeHxpDugJLVICWg4eW44kOQEGJ1r6jCGw==, + } + engines: { node: ^20.19.0 || >=22.12.0 } cpu: [arm64] os: [android] - '@rolldown/binding-darwin-arm64@1.2.3': - resolution: {integrity: sha512-ieIiibVCp0tX7TLu2cafoNPv8wJyYi01ekXpbf8q2j7F4rGAhhXb/eQh7ge9DRBY78GwmRQtvjZDux7EDbA8kA==} - engines: {node: ^20.19.0 || >=22.12.0} + "@rolldown/binding-darwin-arm64@1.2.3": + resolution: + { + integrity: sha512-ieIiibVCp0tX7TLu2cafoNPv8wJyYi01ekXpbf8q2j7F4rGAhhXb/eQh7ge9DRBY78GwmRQtvjZDux7EDbA8kA==, + } + engines: { node: ^20.19.0 || >=22.12.0 } cpu: [arm64] os: [darwin] - '@rolldown/binding-darwin-x64@1.2.3': - resolution: {integrity: sha512-Zh9tCon19eDXJoihx0rqKhMUlMYqzwj3aPsSuHmI4RWZh62dWUL+DJN4C5YQya5TcQBJU/Fe8+rY0jhXTQITqA==} - engines: {node: ^20.19.0 || >=22.12.0} + "@rolldown/binding-darwin-x64@1.2.3": + resolution: + { + integrity: sha512-Zh9tCon19eDXJoihx0rqKhMUlMYqzwj3aPsSuHmI4RWZh62dWUL+DJN4C5YQya5TcQBJU/Fe8+rY0jhXTQITqA==, + } + engines: { node: ^20.19.0 || >=22.12.0 } cpu: [x64] os: [darwin] - '@rolldown/binding-freebsd-x64@1.2.3': - resolution: {integrity: sha512-nGbJWewA1wrXXZiQhjAT5rhibGfns5ZNkDVqxsO6zJ3f3YvpoDNNmGMSbbhLuXKjNScaBJVOAboztAWVespQMg==} - engines: {node: ^20.19.0 || >=22.12.0} + "@rolldown/binding-freebsd-x64@1.2.3": + resolution: + { + integrity: sha512-nGbJWewA1wrXXZiQhjAT5rhibGfns5ZNkDVqxsO6zJ3f3YvpoDNNmGMSbbhLuXKjNScaBJVOAboztAWVespQMg==, + } + engines: { node: ^20.19.0 || >=22.12.0 } cpu: [x64] os: [freebsd] - '@rolldown/binding-linux-arm-gnueabihf@1.2.3': - resolution: {integrity: sha512-QNniJr5Kml0kDEB98jiDOJjXNroxIIi0IXIbdYzY26Xt1pVbeP62+KnoIZLwirOymX/0jDk/2gI/bNUv7A7OIw==} - engines: {node: ^20.19.0 || >=22.12.0} + "@rolldown/binding-linux-arm-gnueabihf@1.2.3": + resolution: + { + integrity: sha512-QNniJr5Kml0kDEB98jiDOJjXNroxIIi0IXIbdYzY26Xt1pVbeP62+KnoIZLwirOymX/0jDk/2gI/bNUv7A7OIw==, + } + engines: { node: ^20.19.0 || >=22.12.0 } cpu: [arm] os: [linux] - '@rolldown/binding-linux-arm64-gnu@1.2.3': - resolution: {integrity: sha512-TkqEAcmmvH3I/q4114NB4RVt6241Dao48pF45uLcFGrwAaIn0iITgTAKP/dLjbN0R4buJjGb91+UHSoFmpgIWw==} - engines: {node: ^20.19.0 || >=22.12.0} + "@rolldown/binding-linux-arm64-gnu@1.2.3": + resolution: + { + integrity: sha512-TkqEAcmmvH3I/q4114NB4RVt6241Dao48pF45uLcFGrwAaIn0iITgTAKP/dLjbN0R4buJjGb91+UHSoFmpgIWw==, + } + engines: { node: ^20.19.0 || >=22.12.0 } cpu: [arm64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-arm64-musl@1.2.3': - resolution: {integrity: sha512-NHqjnxpsndf4MPymxteFAWHHfkTL8HjWh1KB7z23ofZ6QO2euONuxDXjat69dKZRALnGypg8k8SsK8vZJoXv1Q==} - engines: {node: ^20.19.0 || >=22.12.0} + "@rolldown/binding-linux-arm64-musl@1.2.3": + resolution: + { + integrity: sha512-NHqjnxpsndf4MPymxteFAWHHfkTL8HjWh1KB7z23ofZ6QO2euONuxDXjat69dKZRALnGypg8k8SsK8vZJoXv1Q==, + } + engines: { node: ^20.19.0 || >=22.12.0 } cpu: [arm64] os: [linux] libc: [musl] - '@rolldown/binding-linux-ppc64-gnu@1.2.3': - resolution: {integrity: sha512-6tbrbwfz5GB9DQ4Jwo6hy9v+vR31xZlvzZ6n5Xut6Hhx5PvrA9q/HsK8KMaYQp063iqZGXwNvZtYNLD7EM/x0w==} - engines: {node: ^20.19.0 || >=22.12.0} + "@rolldown/binding-linux-ppc64-gnu@1.2.3": + resolution: + { + integrity: sha512-6tbrbwfz5GB9DQ4Jwo6hy9v+vR31xZlvzZ6n5Xut6Hhx5PvrA9q/HsK8KMaYQp063iqZGXwNvZtYNLD7EM/x0w==, + } + engines: { node: ^20.19.0 || >=22.12.0 } cpu: [ppc64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-s390x-gnu@1.2.3': - resolution: {integrity: sha512-oyuXxXmoZHjXC917IAPFAAv4wWAa0cM9afk8nx1+9/jNNOX1uPf8yDA6p7G0RypOfw/X0PQt5IfoquY1um+zSg==} - engines: {node: ^20.19.0 || >=22.12.0} + "@rolldown/binding-linux-s390x-gnu@1.2.3": + resolution: + { + integrity: sha512-oyuXxXmoZHjXC917IAPFAAv4wWAa0cM9afk8nx1+9/jNNOX1uPf8yDA6p7G0RypOfw/X0PQt5IfoquY1um+zSg==, + } + engines: { node: ^20.19.0 || >=22.12.0 } cpu: [s390x] os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-gnu@1.2.3': - resolution: {integrity: sha512-TytMwF2KVGqP2tgd0I1OY0PAv78dZRAYcF5ssDzjM34SUXCED3uXvSd5+lHoC0bTD6eEdFz7LdQNCO1y0oVk9w==} - engines: {node: ^20.19.0 || >=22.12.0} + "@rolldown/binding-linux-x64-gnu@1.2.3": + resolution: + { + integrity: sha512-TytMwF2KVGqP2tgd0I1OY0PAv78dZRAYcF5ssDzjM34SUXCED3uXvSd5+lHoC0bTD6eEdFz7LdQNCO1y0oVk9w==, + } + engines: { node: ^20.19.0 || >=22.12.0 } cpu: [x64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-musl@1.2.3': - resolution: {integrity: sha512-/E9m3qstrJFVPoULV25mVQblSNExY2+kBsYe4sy0Tn0yOOgJ8wZbZt3KnRbF/XeU2Gl1STKUQnDNTqhIE5MD4A==} - engines: {node: ^20.19.0 || >=22.12.0} + "@rolldown/binding-linux-x64-musl@1.2.3": + resolution: + { + integrity: sha512-/E9m3qstrJFVPoULV25mVQblSNExY2+kBsYe4sy0Tn0yOOgJ8wZbZt3KnRbF/XeU2Gl1STKUQnDNTqhIE5MD4A==, + } + engines: { node: ^20.19.0 || >=22.12.0 } cpu: [x64] os: [linux] libc: [musl] - '@rolldown/binding-openharmony-arm64@1.2.3': - resolution: {integrity: sha512-Kr0OcsoQI816i6HOl3vFHpd1K0eZyh76zgfj4c1nTyaTsd5r2Mj1lwM4R90y/qaCfmTn9eHy0SKwi98eitRxug==} - engines: {node: ^20.19.0 || >=22.12.0} + "@rolldown/binding-openharmony-arm64@1.2.3": + resolution: + { + integrity: sha512-Kr0OcsoQI816i6HOl3vFHpd1K0eZyh76zgfj4c1nTyaTsd5r2Mj1lwM4R90y/qaCfmTn9eHy0SKwi98eitRxug==, + } + engines: { node: ^20.19.0 || >=22.12.0 } cpu: [arm64] os: [openharmony] - '@rolldown/binding-win32-arm64-msvc@1.2.3': - resolution: {integrity: sha512-hOtMwTqnME+/gJcH/PCZ0wn0zPUjiWOgkHpxbSJpfGKMezHltx1S7/k1SitzVa7Ww2cqrDDaFbZEhcJZO8o+Jw==} - engines: {node: ^20.19.0 || >=22.12.0} + "@rolldown/binding-win32-arm64-msvc@1.2.3": + resolution: + { + integrity: sha512-hOtMwTqnME+/gJcH/PCZ0wn0zPUjiWOgkHpxbSJpfGKMezHltx1S7/k1SitzVa7Ww2cqrDDaFbZEhcJZO8o+Jw==, + } + engines: { node: ^20.19.0 || >=22.12.0 } cpu: [arm64] os: [win32] - '@rolldown/binding-win32-x64-msvc@1.2.3': - resolution: {integrity: sha512-ekcqMMkI2PlhYnfzQnB/cEdYUVVJViWvoUyLrbzgDoi3Snfc1mVBwdnc306ufA5ejy8JSPjT2RlW1nQSjW7efg==} - engines: {node: ^20.19.0 || >=22.12.0} + "@rolldown/binding-win32-x64-msvc@1.2.3": + resolution: + { + integrity: sha512-ekcqMMkI2PlhYnfzQnB/cEdYUVVJViWvoUyLrbzgDoi3Snfc1mVBwdnc306ufA5ejy8JSPjT2RlW1nQSjW7efg==, + } + engines: { node: ^20.19.0 || >=22.12.0 } cpu: [x64] os: [win32] - '@rolldown/pluginutils@1.0.1': - resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} - - '@rollup/plugin-alias@6.0.0': - resolution: {integrity: sha512-tPCzJOtS7uuVZd+xPhoy5W4vThe6KWXNmsFCNktaAh5RTqcLiSfT4huPQIXkgJ6YCOjJHvecOAzQxLFhPxKr+g==} - engines: {node: '>=20.19.0'} + "@rolldown/pluginutils@1.0.1": + resolution: + { + integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==, + } + + "@rollup/plugin-alias@6.0.0": + resolution: + { + integrity: sha512-tPCzJOtS7uuVZd+xPhoy5W4vThe6KWXNmsFCNktaAh5RTqcLiSfT4huPQIXkgJ6YCOjJHvecOAzQxLFhPxKr+g==, + } + engines: { node: ">=20.19.0" } peerDependencies: - rollup: '>=4.0.0' + rollup: ">=4.0.0" peerDependenciesMeta: rollup: optional: true - '@rollup/plugin-commonjs@29.0.3': - resolution: {integrity: sha512-ZaOxZceP7SOUW7Lqw5IRVweSQYWaeIPnXIGLiB690EBA3FGJTO40EEr2L5yZplJWsgTCogILRSpcAe7+U0Otdg==} - engines: {node: '>=16.0.0 || 14 >= 14.17'} + "@rollup/plugin-commonjs@29.0.3": + resolution: + { + integrity: sha512-ZaOxZceP7SOUW7Lqw5IRVweSQYWaeIPnXIGLiB690EBA3FGJTO40EEr2L5yZplJWsgTCogILRSpcAe7+U0Otdg==, + } + engines: { node: ">=16.0.0 || 14 >= 14.17" } peerDependencies: rollup: ^2.68.0||^3.0.0||^4.0.0 peerDependenciesMeta: rollup: optional: true - '@rollup/plugin-inject@5.0.5': - resolution: {integrity: sha512-2+DEJbNBoPROPkgTDNe8/1YXWcqxbN5DTjASVIOx8HS+pITXushyNiBV56RB08zuptzz8gT3YfkqriTBVycepg==} - engines: {node: '>=14.0.0'} + "@rollup/plugin-inject@5.0.5": + resolution: + { + integrity: sha512-2+DEJbNBoPROPkgTDNe8/1YXWcqxbN5DTjASVIOx8HS+pITXushyNiBV56RB08zuptzz8gT3YfkqriTBVycepg==, + } + engines: { node: ">=14.0.0" } peerDependencies: rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 peerDependenciesMeta: rollup: optional: true - '@rollup/plugin-json@6.1.0': - resolution: {integrity: sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA==} - engines: {node: '>=14.0.0'} + "@rollup/plugin-json@6.1.0": + resolution: + { + integrity: sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA==, + } + engines: { node: ">=14.0.0" } peerDependencies: rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 peerDependenciesMeta: rollup: optional: true - '@rollup/plugin-node-resolve@16.0.3': - resolution: {integrity: sha512-lUYM3UBGuM93CnMPG1YocWu7X802BrNF3jW2zny5gQyLQgRFJhV1Sq0Zi74+dh/6NBx1DxFC4b4GXg9wUCG5Qg==} - engines: {node: '>=14.0.0'} + "@rollup/plugin-node-resolve@16.0.3": + resolution: + { + integrity: sha512-lUYM3UBGuM93CnMPG1YocWu7X802BrNF3jW2zny5gQyLQgRFJhV1Sq0Zi74+dh/6NBx1DxFC4b4GXg9wUCG5Qg==, + } + engines: { node: ">=14.0.0" } peerDependencies: rollup: ^2.78.0||^3.0.0||^4.0.0 peerDependenciesMeta: rollup: optional: true - '@rollup/plugin-replace@6.0.3': - resolution: {integrity: sha512-J4RZarRvQAm5IF0/LwUUg+obsm+xZhYnbMXmXROyoSE1ATJe3oXSb9L5MMppdxP2ylNSjv6zFBwKYjcKMucVfA==} - engines: {node: '>=14.0.0'} + "@rollup/plugin-replace@6.0.3": + resolution: + { + integrity: sha512-J4RZarRvQAm5IF0/LwUUg+obsm+xZhYnbMXmXROyoSE1ATJe3oXSb9L5MMppdxP2ylNSjv6zFBwKYjcKMucVfA==, + } + engines: { node: ">=14.0.0" } peerDependencies: rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 peerDependenciesMeta: rollup: optional: true - '@rollup/plugin-terser@1.0.0': - resolution: {integrity: sha512-FnCxhTBx6bMOYQrar6C8h3scPt8/JwIzw3+AJ2K++6guogH5fYaIFia+zZuhqv0eo1RN7W1Pz630SyvLbDjhtQ==} - engines: {node: '>=20.0.0'} + "@rollup/plugin-terser@1.0.0": + resolution: + { + integrity: sha512-FnCxhTBx6bMOYQrar6C8h3scPt8/JwIzw3+AJ2K++6guogH5fYaIFia+zZuhqv0eo1RN7W1Pz630SyvLbDjhtQ==, + } + engines: { node: ">=20.0.0" } peerDependencies: rollup: ^2.0.0||^3.0.0||^4.0.0 peerDependenciesMeta: rollup: optional: true - '@rollup/pluginutils@5.4.0': - resolution: {integrity: sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg==} - engines: {node: '>=14.0.0'} + "@rollup/pluginutils@5.4.0": + resolution: + { + integrity: sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg==, + } + engines: { node: ">=14.0.0" } peerDependencies: rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 peerDependenciesMeta: rollup: optional: true - '@rollup/rollup-android-arm-eabi@4.62.4': - resolution: {integrity: sha512-RrPokAb7dmbxFoeO3TloqHyOjgye8RkBhSqmp4aJMIex4c9r46ZstPnleDQOq1t46VOVjwIuwNogIqbodV1Vvg==} + "@rollup/rollup-android-arm-eabi@4.62.4": + resolution: + { + integrity: sha512-RrPokAb7dmbxFoeO3TloqHyOjgye8RkBhSqmp4aJMIex4c9r46ZstPnleDQOq1t46VOVjwIuwNogIqbodV1Vvg==, + } cpu: [arm] os: [android] - '@rollup/rollup-android-arm64@4.62.4': - resolution: {integrity: sha512-JKuJc+pnpks2pjy7L/N3v/cAkZxYlnmuZoD840ldbMI5KDbC4iO9NKwPKYdjYFCMAIIlBzYSFHxIJVYzRo2/8A==} + "@rollup/rollup-android-arm64@4.62.4": + resolution: + { + integrity: sha512-JKuJc+pnpks2pjy7L/N3v/cAkZxYlnmuZoD840ldbMI5KDbC4iO9NKwPKYdjYFCMAIIlBzYSFHxIJVYzRo2/8A==, + } cpu: [arm64] os: [android] - '@rollup/rollup-darwin-arm64@4.62.4': - resolution: {integrity: sha512-krw5uS2STmvJ02x0uTXHbqQNuz+9eZ1iw+qXk9dmW2gvV4jV7O2hEoOnuhFrpOPiel1mBFtqbxYZZtC46hXLOw==} + "@rollup/rollup-darwin-arm64@4.62.4": + resolution: + { + integrity: sha512-krw5uS2STmvJ02x0uTXHbqQNuz+9eZ1iw+qXk9dmW2gvV4jV7O2hEoOnuhFrpOPiel1mBFtqbxYZZtC46hXLOw==, + } cpu: [arm64] os: [darwin] - '@rollup/rollup-darwin-x64@4.62.4': - resolution: {integrity: sha512-wsTxtgApb4PrOsNJIm0FZ1h3WvCC+k9uxLJ4ad75hgoS4NiRes2SoJFlDAyMwiUY8IssDqGcHbXuN0sx1tfF1A==} + "@rollup/rollup-darwin-x64@4.62.4": + resolution: + { + integrity: sha512-wsTxtgApb4PrOsNJIm0FZ1h3WvCC+k9uxLJ4ad75hgoS4NiRes2SoJFlDAyMwiUY8IssDqGcHbXuN0sx1tfF1A==, + } cpu: [x64] os: [darwin] - '@rollup/rollup-freebsd-arm64@4.62.4': - resolution: {integrity: sha512-GUOnQlyZe3yAXhWOtOMsn5Qkrv5E5mZXa0thbARWi5Ei2szlVXJFQhddZ4HbAzh8q92w5twp+CQvs/eFanz9YQ==} + "@rollup/rollup-freebsd-arm64@4.62.4": + resolution: + { + integrity: sha512-GUOnQlyZe3yAXhWOtOMsn5Qkrv5E5mZXa0thbARWi5Ei2szlVXJFQhddZ4HbAzh8q92w5twp+CQvs/eFanz9YQ==, + } cpu: [arm64] os: [freebsd] - '@rollup/rollup-freebsd-x64@4.62.4': - resolution: {integrity: sha512-/Y7f3QuxjzPKsjA/rfEDa3+0vXqyjmJ50Ln8dPpCmWkKTrUoWHG1cWhTqaAMLob2m2nESWuC7yGrREz019Ztqg==} + "@rollup/rollup-freebsd-x64@4.62.4": + resolution: + { + integrity: sha512-/Y7f3QuxjzPKsjA/rfEDa3+0vXqyjmJ50Ln8dPpCmWkKTrUoWHG1cWhTqaAMLob2m2nESWuC7yGrREz019Ztqg==, + } cpu: [x64] os: [freebsd] - '@rollup/rollup-linux-arm-gnueabihf@4.62.4': - resolution: {integrity: sha512-81wiiX3v7aqy+T+bT61TJ78yJjRquqFFTTbAPt08imfQQzkPIW8t6aJbkTagtCCrXMNc9D66+geqlK7ydLPNqA==} + "@rollup/rollup-linux-arm-gnueabihf@4.62.4": + resolution: + { + integrity: sha512-81wiiX3v7aqy+T+bT61TJ78yJjRquqFFTTbAPt08imfQQzkPIW8t6aJbkTagtCCrXMNc9D66+geqlK7ydLPNqA==, + } cpu: [arm] os: [linux] libc: [glibc] - '@rollup/rollup-linux-arm-musleabihf@4.62.4': - resolution: {integrity: sha512-9kmDIvNZqdoHOBZgNtpTBeLWYO/LVipM3H/j62P8848/l/VPEQL6N3uxU9pvP1oZAsXyC2MEnFP3ovRjo7WYNQ==} + "@rollup/rollup-linux-arm-musleabihf@4.62.4": + resolution: + { + integrity: sha512-9kmDIvNZqdoHOBZgNtpTBeLWYO/LVipM3H/j62P8848/l/VPEQL6N3uxU9pvP1oZAsXyC2MEnFP3ovRjo7WYNQ==, + } cpu: [arm] os: [linux] libc: [musl] - '@rollup/rollup-linux-arm64-gnu@4.62.4': - resolution: {integrity: sha512-CcnXHWnXg69g+DX5VWL3FHts3qMRN2uVEHX+BZvGLdd07/gXkn3ePjYtO1LDJvxkGKVHMclKBRa1QUTH+6toYQ==} + "@rollup/rollup-linux-arm64-gnu@4.62.4": + resolution: + { + integrity: sha512-CcnXHWnXg69g+DX5VWL3FHts3qMRN2uVEHX+BZvGLdd07/gXkn3ePjYtO1LDJvxkGKVHMclKBRa1QUTH+6toYQ==, + } cpu: [arm64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-arm64-musl@4.62.4': - resolution: {integrity: sha512-iFOibiHnTRuhrWLlRsOQFdZJJIa7S8OwkneJr4ocALP16u5yk6lWLINFwhHaEqBFMsKDUZofLkGos7+CPzGB3g==} + "@rollup/rollup-linux-arm64-musl@4.62.4": + resolution: + { + integrity: sha512-iFOibiHnTRuhrWLlRsOQFdZJJIa7S8OwkneJr4ocALP16u5yk6lWLINFwhHaEqBFMsKDUZofLkGos7+CPzGB3g==, + } cpu: [arm64] os: [linux] libc: [musl] - '@rollup/rollup-linux-loong64-gnu@4.62.4': - resolution: {integrity: sha512-XnWYMI7euHlb5a871xPja+Gm7DRCFU+FGRrtS2sMq9N8FvqtpagUy6gD4YOemC5MRk9xbh8+jYMEJbigFQwsgA==} + "@rollup/rollup-linux-loong64-gnu@4.62.4": + resolution: + { + integrity: sha512-XnWYMI7euHlb5a871xPja+Gm7DRCFU+FGRrtS2sMq9N8FvqtpagUy6gD4YOemC5MRk9xbh8+jYMEJbigFQwsgA==, + } cpu: [loong64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-loong64-musl@4.62.4': - resolution: {integrity: sha512-qGDAlO0U8xedCcsdRm9oaoQY8DAx/QT7uIxJWhCdx0ceIWX783UC9QSYkdpzAe29wNiVfp24+bZdQmn49o45SQ==} + "@rollup/rollup-linux-loong64-musl@4.62.4": + resolution: + { + integrity: sha512-qGDAlO0U8xedCcsdRm9oaoQY8DAx/QT7uIxJWhCdx0ceIWX783UC9QSYkdpzAe29wNiVfp24+bZdQmn49o45SQ==, + } cpu: [loong64] os: [linux] libc: [musl] - '@rollup/rollup-linux-ppc64-gnu@4.62.4': - resolution: {integrity: sha512-ru4H6ezD7ysA5EiEK6qkkaEb4modH8CTej6kUy/gQi20u3kB3G7Zn8snXXkeJSCOFKG/rbPPtM/+9Wgas1961w==} + "@rollup/rollup-linux-ppc64-gnu@4.62.4": + resolution: + { + integrity: sha512-ru4H6ezD7ysA5EiEK6qkkaEb4modH8CTej6kUy/gQi20u3kB3G7Zn8snXXkeJSCOFKG/rbPPtM/+9Wgas1961w==, + } cpu: [ppc64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-ppc64-musl@4.62.4': - resolution: {integrity: sha512-2W4MO5WQVJnbJaZdvDb9rhBDuFU1nKIepPFpJUBsTh2k1YY2g+ODViaWuyOAjQ5cOP7NvrvLzt3wvHOoiAvc7w==} + "@rollup/rollup-linux-ppc64-musl@4.62.4": + resolution: + { + integrity: sha512-2W4MO5WQVJnbJaZdvDb9rhBDuFU1nKIepPFpJUBsTh2k1YY2g+ODViaWuyOAjQ5cOP7NvrvLzt3wvHOoiAvc7w==, + } cpu: [ppc64] os: [linux] libc: [musl] - '@rollup/rollup-linux-riscv64-gnu@4.62.4': - resolution: {integrity: sha512-+fxjfuoAmVMCYV5QyjoIpu0cp5DOiOTeqYFk1AVaxGr+/ravWLX89XfQmptsoWcaVy/TGf2hexzbUOrCQIL1CQ==} + "@rollup/rollup-linux-riscv64-gnu@4.62.4": + resolution: + { + integrity: sha512-+fxjfuoAmVMCYV5QyjoIpu0cp5DOiOTeqYFk1AVaxGr+/ravWLX89XfQmptsoWcaVy/TGf2hexzbUOrCQIL1CQ==, + } cpu: [riscv64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-riscv64-musl@4.62.4': - resolution: {integrity: sha512-jTn8JfHGL4djjFxPuM06LmNUJDsst2jeVlsd9OmIH6zc5sC9K6rIuO4YajXatLUpBmBKl6b35ro1QZocLi+tcA==} + "@rollup/rollup-linux-riscv64-musl@4.62.4": + resolution: + { + integrity: sha512-jTn8JfHGL4djjFxPuM06LmNUJDsst2jeVlsd9OmIH6zc5sC9K6rIuO4YajXatLUpBmBKl6b35ro1QZocLi+tcA==, + } cpu: [riscv64] os: [linux] libc: [musl] - '@rollup/rollup-linux-s390x-gnu@4.62.4': - resolution: {integrity: sha512-oCJCJL4pXsoDcP2QZ+JVlPTIRc6266zsIaeJJsWImmF7HO0W8nb6HuSgZlMWxJwaPf8ehbSw8yo0EUw925hKsA==} + "@rollup/rollup-linux-s390x-gnu@4.62.4": + resolution: + { + integrity: sha512-oCJCJL4pXsoDcP2QZ+JVlPTIRc6266zsIaeJJsWImmF7HO0W8nb6HuSgZlMWxJwaPf8ehbSw8yo0EUw925hKsA==, + } cpu: [s390x] os: [linux] libc: [glibc] - '@rollup/rollup-linux-x64-gnu@4.62.4': - resolution: {integrity: sha512-W69hukhZ3KKNRCaMIEzKvcFye42hh0FE1+YoYaf5+Ikacuftoco6yO/xouz0hc5d5W/s3yBro5jRiuEE/Q5vUw==} + "@rollup/rollup-linux-x64-gnu@4.62.4": + resolution: + { + integrity: sha512-W69hukhZ3KKNRCaMIEzKvcFye42hh0FE1+YoYaf5+Ikacuftoco6yO/xouz0hc5d5W/s3yBro5jRiuEE/Q5vUw==, + } cpu: [x64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-x64-musl@4.62.4': - resolution: {integrity: sha512-qiXbGG2jkjXhzXpsFZSR2Xpb8DN/UaxYsbb/STbuR/6fpaDgRmmaq1B/LmtF2wQFOFOSsK2jdE0RZ3a0zHn4QA==} + "@rollup/rollup-linux-x64-musl@4.62.4": + resolution: + { + integrity: sha512-qiXbGG2jkjXhzXpsFZSR2Xpb8DN/UaxYsbb/STbuR/6fpaDgRmmaq1B/LmtF2wQFOFOSsK2jdE0RZ3a0zHn4QA==, + } cpu: [x64] os: [linux] libc: [musl] - '@rollup/rollup-openbsd-x64@4.62.4': - resolution: {integrity: sha512-nWeM//hxv8mIo6jD7Hu4o48DVmV9pbV6gsKaWU+4NFyqHoPKwrkRiZGLKUhOBk8qNmDmpwFtPKg80Bo/Tn4xiQ==} + "@rollup/rollup-openbsd-x64@4.62.4": + resolution: + { + integrity: sha512-nWeM//hxv8mIo6jD7Hu4o48DVmV9pbV6gsKaWU+4NFyqHoPKwrkRiZGLKUhOBk8qNmDmpwFtPKg80Bo/Tn4xiQ==, + } cpu: [x64] os: [openbsd] - '@rollup/rollup-openharmony-arm64@4.62.4': - resolution: {integrity: sha512-s62SQ/vgsRSvMwDkOEfTqfgASF0f26ZNaQuTA6Aok5lrikf89yI2W0gFHvZb2Jpgc6N8JnOKZgCK2iciO3CsxQ==} + "@rollup/rollup-openharmony-arm64@4.62.4": + resolution: + { + integrity: sha512-s62SQ/vgsRSvMwDkOEfTqfgASF0f26ZNaQuTA6Aok5lrikf89yI2W0gFHvZb2Jpgc6N8JnOKZgCK2iciO3CsxQ==, + } cpu: [arm64] os: [openharmony] - '@rollup/rollup-win32-arm64-msvc@4.62.4': - resolution: {integrity: sha512-J6wGf8TVGbXJq+HH+ttTvrcfNKPbuZecV6KT1B8I18BC5IURUh5kl4Yl5OEP5eFIUoI5BWxCsyYMhFsDx8kekw==} + "@rollup/rollup-win32-arm64-msvc@4.62.4": + resolution: + { + integrity: sha512-J6wGf8TVGbXJq+HH+ttTvrcfNKPbuZecV6KT1B8I18BC5IURUh5kl4Yl5OEP5eFIUoI5BWxCsyYMhFsDx8kekw==, + } cpu: [arm64] os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.62.4': - resolution: {integrity: sha512-zmfrQd/0wu6oJs8Vq8KwY/YtsKSsLtKe/HwAP4Wqy8LhWjeT55fHRAkOhYQ12wI3ayS4Tt12d5CDRD7N96SAYQ==} + "@rollup/rollup-win32-ia32-msvc@4.62.4": + resolution: + { + integrity: sha512-zmfrQd/0wu6oJs8Vq8KwY/YtsKSsLtKe/HwAP4Wqy8LhWjeT55fHRAkOhYQ12wI3ayS4Tt12d5CDRD7N96SAYQ==, + } cpu: [ia32] os: [win32] - '@rollup/rollup-win32-x64-gnu@4.62.4': - resolution: {integrity: sha512-qPzHqdj9rfUD+w79dtE07zi/kFwKyCJqplp5K5ygeLTp7jLpAoc16OAH39HSmRC9UpozaecsleI8uAdEj6v2yw==} + "@rollup/rollup-win32-x64-gnu@4.62.4": + resolution: + { + integrity: sha512-qPzHqdj9rfUD+w79dtE07zi/kFwKyCJqplp5K5ygeLTp7jLpAoc16OAH39HSmRC9UpozaecsleI8uAdEj6v2yw==, + } cpu: [x64] os: [win32] - '@rollup/rollup-win32-x64-msvc@4.62.4': - resolution: {integrity: sha512-zD6NdeWEByGE9QF9vCrlJ5YQB4oq9q91kPZS37Jwj5hOkvR1lTBSpsKhKDw4IJtbQ35LsTS1HD9DZYGKIshU1Q==} + "@rollup/rollup-win32-x64-msvc@4.62.4": + resolution: + { + integrity: sha512-zD6NdeWEByGE9QF9vCrlJ5YQB4oq9q91kPZS37Jwj5hOkvR1lTBSpsKhKDw4IJtbQ35LsTS1HD9DZYGKIshU1Q==, + } cpu: [x64] os: [win32] - '@simple-git/args-pathspec@1.0.3': - resolution: {integrity: sha512-ngJMaHlsWDTfjyq9F3VIQ8b7NXbBLq5j9i5bJ6XLYtD6qlDXT7fdKY2KscWWUF8t18xx052Y/PUO1K1TRc9yKA==} - - '@simple-git/argv-parser@1.1.1': - resolution: {integrity: sha512-Q9lBcfQ+VQCpQqGJFHe5yooOS5hGdLFFbJ5R+R5aDsnkPCahtn1hSkMcORX65J2Z5lxSkD0lQorMsncuBQxYUw==} - - '@sindresorhus/is@7.2.0': - resolution: {integrity: sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==} - engines: {node: '>=18'} - - '@sindresorhus/merge-streams@4.0.0': - resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} - engines: {node: '>=18'} - - '@speed-highlight/core@1.2.23': - resolution: {integrity: sha512-iRoq6i6JDJP6Mt2A5JaPvzw0pgYHH6k92ij+yXiTrB7T2y9N789aWE3EHWj/5ztlJBokcCBja3iYLVdu5wgnkg==} - - '@tailwindcss/forms@0.5.10': - resolution: {integrity: sha512-utI1ONF6uf/pPNO68kmN1b8rEwNXv3czukalo8VtJH8ksIkZXr3Q3VYudZLkCsDd4Wku120uF02hYK25XGPorw==} + "@simple-git/args-pathspec@1.0.3": + resolution: + { + integrity: sha512-ngJMaHlsWDTfjyq9F3VIQ8b7NXbBLq5j9i5bJ6XLYtD6qlDXT7fdKY2KscWWUF8t18xx052Y/PUO1K1TRc9yKA==, + } + + "@simple-git/argv-parser@1.1.1": + resolution: + { + integrity: sha512-Q9lBcfQ+VQCpQqGJFHe5yooOS5hGdLFFbJ5R+R5aDsnkPCahtn1hSkMcORX65J2Z5lxSkD0lQorMsncuBQxYUw==, + } + + "@sindresorhus/is@7.2.0": + resolution: + { + integrity: sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==, + } + engines: { node: ">=18" } + + "@sindresorhus/merge-streams@4.0.0": + resolution: + { + integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==, + } + engines: { node: ">=18" } + + "@speed-highlight/core@1.2.23": + resolution: + { + integrity: sha512-iRoq6i6JDJP6Mt2A5JaPvzw0pgYHH6k92ij+yXiTrB7T2y9N789aWE3EHWj/5ztlJBokcCBja3iYLVdu5wgnkg==, + } + + "@tailwindcss/forms@0.5.10": + resolution: + { + integrity: sha512-utI1ONF6uf/pPNO68kmN1b8rEwNXv3czukalo8VtJH8ksIkZXr3Q3VYudZLkCsDd4Wku120uF02hYK25XGPorw==, + } peerDependencies: - tailwindcss: '>=3.0.0 || >= 3.0.0-alpha.1 || >= 4.0.0-alpha.20 || >= 4.0.0-beta.1' + tailwindcss: ">=3.0.0 || >= 3.0.0-alpha.1 || >= 4.0.0-alpha.20 || >= 4.0.0-beta.1" - '@tailwindcss/typography@0.5.19': - resolution: {integrity: sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg==} + "@tailwindcss/typography@0.5.19": + resolution: + { + integrity: sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg==, + } peerDependencies: - tailwindcss: '>=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1' - - '@tanstack/virtual-core@3.13.12': - resolution: {integrity: sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA==} - - '@tanstack/vue-virtual@3.13.12': - resolution: {integrity: sha512-vhF7kEU9EXWXh+HdAwKJ2m3xaOnTTmgcdXcF2pim8g4GvI7eRrk2YRuV5nUlZnd/NbCIX4/Ja2OZu5EjJL06Ww==} + tailwindcss: ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1" + + "@tanstack/virtual-core@3.13.12": + resolution: + { + integrity: sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA==, + } + + "@tanstack/vue-virtual@3.13.12": + resolution: + { + integrity: sha512-vhF7kEU9EXWXh+HdAwKJ2m3xaOnTTmgcdXcF2pim8g4GvI7eRrk2YRuV5nUlZnd/NbCIX4/Ja2OZu5EjJL06Ww==, + } peerDependencies: vue: ^2.7.0 || ^3.0.0 - '@tauri-apps/api@2.9.1': - resolution: {integrity: sha512-IGlhP6EivjXHepbBic618GOmiWe4URJiIeZFlB7x3czM0yDHHYviH1Xvoiv4FefdkQtn6v7TuwWCRfOGdnVUGw==} - - '@tauri-apps/plugin-dialog@2.6.0': - resolution: {integrity: sha512-q4Uq3eY87TdcYzXACiYSPhmpBA76shgmQswGkSVio4C82Sz2W4iehe9TnKYwbq7weHiL88Yw19XZm7v28+Micg==} - - '@tauri-apps/plugin-os@2.3.2': - resolution: {integrity: sha512-n+nXWeuSeF9wcEsSPmRnBEGrRgOy6jjkSU+UVCOV8YUGKb2erhDOxis7IqRXiRVHhY8XMKks00BJ0OAdkpf6+A==} - - '@tauri-apps/plugin-shell@2.3.3': - resolution: {integrity: sha512-Xod+pRcFxmOWFWEnqH5yZcA7qwAMuaaDkMR1Sply+F8VfBj++CGnj2xf5UoialmjZ2Cvd8qrvSCbU+7GgNVsKQ==} - - '@types/debug@4.1.12': - resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} - - '@types/estree@1.0.9': - resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} - - '@types/jsesc@2.5.1': - resolution: {integrity: sha512-9VN+6yxLOPLOav+7PwjZbxiID2bVaeq0ED4qSQmdQTdjnXJSaCVKTR58t15oqH1H5t8Ng2ZX1SabJVoN9Q34bw==} - - '@types/linkify-it@5.0.0': - resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==} - - '@types/markdown-it@14.1.2': - resolution: {integrity: sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==} - - '@types/mdurl@2.0.0': - resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==} - - '@types/ms@2.1.0': - resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} - - '@types/node@25.5.0': - resolution: {integrity: sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==} - - '@types/resolve@1.20.2': - resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} - - '@unhead/bundler@3.3.1': - resolution: {integrity: sha512-F9gEgqpUKqHFzOv+7Pgm3TbmXhUdLX+WjZiPAK/huLDzblzZmvAUE9vRDymWaOST7jqGT/tPKkwl2kqbgb3nPw==} + "@tauri-apps/api@2.9.1": + resolution: + { + integrity: sha512-IGlhP6EivjXHepbBic618GOmiWe4URJiIeZFlB7x3czM0yDHHYviH1Xvoiv4FefdkQtn6v7TuwWCRfOGdnVUGw==, + } + + "@tauri-apps/plugin-dialog@2.6.0": + resolution: + { + integrity: sha512-q4Uq3eY87TdcYzXACiYSPhmpBA76shgmQswGkSVio4C82Sz2W4iehe9TnKYwbq7weHiL88Yw19XZm7v28+Micg==, + } + + "@tauri-apps/plugin-os@2.3.2": + resolution: + { + integrity: sha512-n+nXWeuSeF9wcEsSPmRnBEGrRgOy6jjkSU+UVCOV8YUGKb2erhDOxis7IqRXiRVHhY8XMKks00BJ0OAdkpf6+A==, + } + + "@tauri-apps/plugin-shell@2.3.3": + resolution: + { + integrity: sha512-Xod+pRcFxmOWFWEnqH5yZcA7qwAMuaaDkMR1Sply+F8VfBj++CGnj2xf5UoialmjZ2Cvd8qrvSCbU+7GgNVsKQ==, + } + + "@types/debug@4.1.12": + resolution: + { + integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==, + } + + "@types/estree@1.0.9": + resolution: + { + integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==, + } + + "@types/jsesc@2.5.1": + resolution: + { + integrity: sha512-9VN+6yxLOPLOav+7PwjZbxiID2bVaeq0ED4qSQmdQTdjnXJSaCVKTR58t15oqH1H5t8Ng2ZX1SabJVoN9Q34bw==, + } + + "@types/linkify-it@5.0.0": + resolution: + { + integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==, + } + + "@types/markdown-it@14.1.2": + resolution: + { + integrity: sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==, + } + + "@types/mdurl@2.0.0": + resolution: + { + integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==, + } + + "@types/ms@2.1.0": + resolution: + { + integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==, + } + + "@types/node@25.5.0": + resolution: + { + integrity: sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==, + } + + "@types/resolve@1.20.2": + resolution: + { + integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==, + } + + "@unhead/bundler@3.3.1": + resolution: + { + integrity: sha512-F9gEgqpUKqHFzOv+7Pgm3TbmXhUdLX+WjZiPAK/huLDzblzZmvAUE9vRDymWaOST7jqGT/tPKkwl2kqbgb3nPw==, + } peerDependencies: - '@unhead/cli': ^3.3.1 - '@vitejs/devtools-kit': ^0.4.1 - esbuild: '>=0.17.0' - lightningcss: '>=1.20.0' - oxc-parser: '>=0.98.0' - rolldown: '>=1.0.0' + "@unhead/cli": ^3.3.1 + "@vitejs/devtools-kit": ^0.4.1 + esbuild: ">=0.17.0" + lightningcss: ">=1.20.0" + oxc-parser: ">=0.98.0" + rolldown: ">=1.0.0" unhead: ^3.3.1 - vite: '>=6.4.2' - webpack: '>=5.0.0' + vite: ">=6.4.2" + webpack: ">=5.0.0" peerDependenciesMeta: - '@unhead/cli': + "@unhead/cli": optional: true - '@vitejs/devtools-kit': + "@vitejs/devtools-kit": optional: true esbuild: optional: true @@ -1142,295 +1719,496 @@ packages: webpack: optional: true - '@unhead/vue@3.3.1': - resolution: {integrity: sha512-iS+eiE1NehbV/eF7wzR7UUuUVTv8fIphFOCekQvEMuPqfKPCB8f3wf7sgPgUngYX6mdgM6PmkGmaOQgTBxqwbw==} + "@unhead/vue@3.3.1": + resolution: + { + integrity: sha512-iS+eiE1NehbV/eF7wzR7UUuUVTv8fIphFOCekQvEMuPqfKPCB8f3wf7sgPgUngYX6mdgM6PmkGmaOQgTBxqwbw==, + } peerDependencies: - vite: '>=6.4.2' - vue: '>=3.5.18' - webpack: '>=5.0.0' + vite: ">=6.4.2" + vue: ">=3.5.18" + webpack: ">=5.0.0" peerDependenciesMeta: vite: optional: true webpack: optional: true - '@vercel/nft@1.10.2': - resolution: {integrity: sha512-w+WyX5Ulmj4dtTZrxaulqrjaLZHSbnPzx75SJsTNYmotKsqn1JlLnDJa+lz5hn90HJofhl/2MAtw0mCrgM3qYw==} - engines: {node: '>=20'} + "@vercel/nft@1.10.2": + resolution: + { + integrity: sha512-w+WyX5Ulmj4dtTZrxaulqrjaLZHSbnPzx75SJsTNYmotKsqn1JlLnDJa+lz5hn90HJofhl/2MAtw0mCrgM3qYw==, + } + engines: { node: ">=20" } hasBin: true - '@vitejs/plugin-vue-jsx@5.1.6': - resolution: {integrity: sha512-YXvi4as2clxt6DFw5+a0tTA97ntiQXm/raR8ofNj3aNwwdlVGTiG2gp7EvfZW17P50acL/9bP0ccF4XnqNmlgA==} - engines: {node: ^20.19.0 || >=22.12.0} + "@vitejs/plugin-vue-jsx@5.1.6": + resolution: + { + integrity: sha512-YXvi4as2clxt6DFw5+a0tTA97ntiQXm/raR8ofNj3aNwwdlVGTiG2gp7EvfZW17P50acL/9bP0ccF4XnqNmlgA==, + } + engines: { node: ^20.19.0 || >=22.12.0 } peerDependencies: vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 vue: ^3.0.0 - '@vitejs/plugin-vue@6.0.8': - resolution: {integrity: sha512-0ZjgOg7oO6farnNGup7yvoM/YXZV84OZxHAwtflItNa/6zzQyVb5LNxyea3FEKEX2XlagIKzrlH7wwxkKgtiew==} - engines: {node: ^20.19.0 || >=22.12.0} + "@vitejs/plugin-vue@6.0.8": + resolution: + { + integrity: sha512-0ZjgOg7oO6farnNGup7yvoM/YXZV84OZxHAwtflItNa/6zzQyVb5LNxyea3FEKEX2XlagIKzrlH7wwxkKgtiew==, + } + engines: { node: ^20.19.0 || >=22.12.0 } peerDependencies: vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 vue: ^3.2.25 - '@volar/language-core@2.4.15': - resolution: {integrity: sha512-3VHw+QZU0ZG9IuQmzT68IyN4hZNd9GchGPhbD9+pa8CVv7rnoOZwo7T8weIbrRmihqy3ATpdfXFnqRrfPVK6CA==} - - '@volar/source-map@2.4.15': - resolution: {integrity: sha512-CPbMWlUN6hVZJYGcU/GSoHu4EnCHiLaXI9n8c9la6RaI9W5JHX+NqG+GSQcB0JdC2FIBLdZJwGsfKyBB71VlTg==} - - '@volar/typescript@2.4.15': - resolution: {integrity: sha512-2aZ8i0cqPGjXb4BhkMsPYDkkuc2ZQ6yOpqwAuNwUoncELqoy5fRgOQtLR9gB0g902iS0NAkvpIzs27geVyVdPg==} - - '@vue-macros/common@3.1.4': - resolution: {integrity: sha512-/5Fv+6DgIcM9ajY05ZmKBv+LMX1M9A0X+IUwDRVdt67ciw8OV9bvG2r34p3RiEadlsQybjhKPRKNXDC8Bp23cw==} - engines: {node: '>=20.19.0'} + "@volar/language-core@2.4.15": + resolution: + { + integrity: sha512-3VHw+QZU0ZG9IuQmzT68IyN4hZNd9GchGPhbD9+pa8CVv7rnoOZwo7T8weIbrRmihqy3ATpdfXFnqRrfPVK6CA==, + } + + "@volar/source-map@2.4.15": + resolution: + { + integrity: sha512-CPbMWlUN6hVZJYGcU/GSoHu4EnCHiLaXI9n8c9la6RaI9W5JHX+NqG+GSQcB0JdC2FIBLdZJwGsfKyBB71VlTg==, + } + + "@volar/typescript@2.4.15": + resolution: + { + integrity: sha512-2aZ8i0cqPGjXb4BhkMsPYDkkuc2ZQ6yOpqwAuNwUoncELqoy5fRgOQtLR9gB0g902iS0NAkvpIzs27geVyVdPg==, + } + + "@vue-macros/common@3.1.4": + resolution: + { + integrity: sha512-/5Fv+6DgIcM9ajY05ZmKBv+LMX1M9A0X+IUwDRVdt67ciw8OV9bvG2r34p3RiEadlsQybjhKPRKNXDC8Bp23cw==, + } + engines: { node: ">=20.19.0" } peerDependencies: vue: ^2.7.0 || ^3.2.25 peerDependenciesMeta: vue: optional: true - '@vue/babel-helper-vue-transform-on@2.0.1': - resolution: {integrity: sha512-uZ66EaFbnnZSYqYEyplWvn46GhZ1KuYSThdT68p+am7MgBNbQ3hphTL9L+xSIsWkdktwhPYLwPgVWqo96jDdRA==} + "@vue/babel-helper-vue-transform-on@2.0.1": + resolution: + { + integrity: sha512-uZ66EaFbnnZSYqYEyplWvn46GhZ1KuYSThdT68p+am7MgBNbQ3hphTL9L+xSIsWkdktwhPYLwPgVWqo96jDdRA==, + } - '@vue/babel-plugin-jsx@2.0.1': - resolution: {integrity: sha512-a8CaLQjD/s4PVdhrLD/zT574ZNPnZBOY+IhdtKWRB4HRZ0I2tXBi5ne7d9eCfaYwp5gU5+4KIyFTV1W1YL9xZA==} + "@vue/babel-plugin-jsx@2.0.1": + resolution: + { + integrity: sha512-a8CaLQjD/s4PVdhrLD/zT574ZNPnZBOY+IhdtKWRB4HRZ0I2tXBi5ne7d9eCfaYwp5gU5+4KIyFTV1W1YL9xZA==, + } peerDependencies: - '@babel/core': ^7.0.0-0 + "@babel/core": ^7.0.0-0 peerDependenciesMeta: - '@babel/core': + "@babel/core": optional: true - '@vue/babel-plugin-resolve-type@2.0.1': - resolution: {integrity: sha512-ybwgIuRGRRBhOU37GImDoWQoz+TlSqap65qVI6iwg/J7FfLTLmMf97TS7xQH9I7Qtr/gp161kYVdhr1ZMraSYQ==} + "@vue/babel-plugin-resolve-type@2.0.1": + resolution: + { + integrity: sha512-ybwgIuRGRRBhOU37GImDoWQoz+TlSqap65qVI6iwg/J7FfLTLmMf97TS7xQH9I7Qtr/gp161kYVdhr1ZMraSYQ==, + } peerDependencies: - '@babel/core': ^7.0.0-0 - - '@vue/compiler-core@3.5.38': - resolution: {integrity: sha512-s99aGxWYig9ErHbct27KXEGhrBYlRI6c4MwAgXErOAbX9xiW37/uMa+XUDO69zLz83dng8UUZ70CTOJrLrYrEQ==} - - '@vue/compiler-core@3.5.40': - resolution: {integrity: sha512-39E8IgOhTbVDnoJFMKc2DvYnypcZwUqgUhQkccva/0m6FUwtIKSGV7n1hpVmYcFaoRAwf9pBcwnKlCEsN63ZEQ==} - - '@vue/compiler-core@3.5.41': - resolution: {integrity: sha512-q0Xtv/F9w2YO/7htQhtiL+Ev2WCJbe5N2hc+XfgyKkEKqWpSxknmT8QOuGdEKNdjPq0c3F7rNpFkTo3Kfrm7pg==} - - '@vue/compiler-dom@3.5.38': - resolution: {integrity: sha512-JTqp25l8aFfJYF7/KmsXZjAxJz7T+SjmTJLoXVjHtc2BrSgSiW2n9Aem/cWq1OPe68A8JL06B3eVdhlP0H4TVw==} - - '@vue/compiler-dom@3.5.40': - resolution: {integrity: sha512-pwkx4vqlqOspFstrcmzwkKLePVMD3PT65imRzLhanU2V1Fj4K13g6OXjanOyzw3aTAuRk84BOmY8f3rEHqPaVA==} - - '@vue/compiler-dom@3.5.41': - resolution: {integrity: sha512-oKacVfNglLvGjnS6BXOlGL7EyG2h8X03pqXCjzotRZUaXGjbrTJUnVAQjrCqUnS+lyu31nwQjZY/d817GmCnfw==} - - '@vue/compiler-sfc@3.5.40': - resolution: {integrity: sha512-gIf497P4kpuALcvs5n3AEg1Vdn0pSY4XbjASIfHNYF1/MP3T2Mf2STERTubysBxCRxzJGJYtF/O7vwJrxFB3Vw==} - - '@vue/compiler-sfc@3.5.41': - resolution: {integrity: sha512-XJhip7R2wy6vX3knCxdZN4KracFaZUef58s1KYewqluedHIJaPIVfXoYT7MF1F8nCvv6k8bWWxDC8opMkg1VTQ==} - - '@vue/compiler-ssr@3.5.40': - resolution: {integrity: sha512-rrE5xiXG663+vHCHa3J9p2z5OcBRjXmoqenprJxAFQxg5pSshzeBiCE6pu46axapRJ2Adk0YDA2BRZVjiHXnhg==} - - '@vue/compiler-ssr@3.5.41': - resolution: {integrity: sha512-U3v5OejKEGqOI0Wy0+Sz7hGuIFZHA4LSXzrNM3IMIeDyJEBBfTpX26n3SDgToRpP2bLc9FfI2j/kSgcJ8Emq5A==} - - '@vue/compiler-vue2@2.7.16': - resolution: {integrity: sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==} - - '@vue/devtools-api@8.2.1': - resolution: {integrity: sha512-6u4vXBlIBAC1wMplIZgpyPn7uh/s4Bf6F5bMzvLv+EdJ0aHs/+4B7Ygv864EStQSjRbsRzTko/kUG1A1IejQ3A==} - - '@vue/devtools-core@8.2.1': - resolution: {integrity: sha512-s/VfAY9oDTb/kFEWmy461jaFde2MIV1RO/gi1vwM+PAZBZ/Pc2Ndu3BNBdZUze8QDUuyYvElbEEGA83syjJfzA==} + "@babel/core": ^7.0.0-0 + + "@vue/compiler-core@3.5.38": + resolution: + { + integrity: sha512-s99aGxWYig9ErHbct27KXEGhrBYlRI6c4MwAgXErOAbX9xiW37/uMa+XUDO69zLz83dng8UUZ70CTOJrLrYrEQ==, + } + + "@vue/compiler-core@3.5.40": + resolution: + { + integrity: sha512-39E8IgOhTbVDnoJFMKc2DvYnypcZwUqgUhQkccva/0m6FUwtIKSGV7n1hpVmYcFaoRAwf9pBcwnKlCEsN63ZEQ==, + } + + "@vue/compiler-core@3.5.41": + resolution: + { + integrity: sha512-q0Xtv/F9w2YO/7htQhtiL+Ev2WCJbe5N2hc+XfgyKkEKqWpSxknmT8QOuGdEKNdjPq0c3F7rNpFkTo3Kfrm7pg==, + } + + "@vue/compiler-dom@3.5.38": + resolution: + { + integrity: sha512-JTqp25l8aFfJYF7/KmsXZjAxJz7T+SjmTJLoXVjHtc2BrSgSiW2n9Aem/cWq1OPe68A8JL06B3eVdhlP0H4TVw==, + } + + "@vue/compiler-dom@3.5.40": + resolution: + { + integrity: sha512-pwkx4vqlqOspFstrcmzwkKLePVMD3PT65imRzLhanU2V1Fj4K13g6OXjanOyzw3aTAuRk84BOmY8f3rEHqPaVA==, + } + + "@vue/compiler-dom@3.5.41": + resolution: + { + integrity: sha512-oKacVfNglLvGjnS6BXOlGL7EyG2h8X03pqXCjzotRZUaXGjbrTJUnVAQjrCqUnS+lyu31nwQjZY/d817GmCnfw==, + } + + "@vue/compiler-sfc@3.5.40": + resolution: + { + integrity: sha512-gIf497P4kpuALcvs5n3AEg1Vdn0pSY4XbjASIfHNYF1/MP3T2Mf2STERTubysBxCRxzJGJYtF/O7vwJrxFB3Vw==, + } + + "@vue/compiler-sfc@3.5.41": + resolution: + { + integrity: sha512-XJhip7R2wy6vX3knCxdZN4KracFaZUef58s1KYewqluedHIJaPIVfXoYT7MF1F8nCvv6k8bWWxDC8opMkg1VTQ==, + } + + "@vue/compiler-ssr@3.5.40": + resolution: + { + integrity: sha512-rrE5xiXG663+vHCHa3J9p2z5OcBRjXmoqenprJxAFQxg5pSshzeBiCE6pu46axapRJ2Adk0YDA2BRZVjiHXnhg==, + } + + "@vue/compiler-ssr@3.5.41": + resolution: + { + integrity: sha512-U3v5OejKEGqOI0Wy0+Sz7hGuIFZHA4LSXzrNM3IMIeDyJEBBfTpX26n3SDgToRpP2bLc9FfI2j/kSgcJ8Emq5A==, + } + + "@vue/compiler-vue2@2.7.16": + resolution: + { + integrity: sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==, + } + + "@vue/devtools-api@8.2.1": + resolution: + { + integrity: sha512-6u4vXBlIBAC1wMplIZgpyPn7uh/s4Bf6F5bMzvLv+EdJ0aHs/+4B7Ygv864EStQSjRbsRzTko/kUG1A1IejQ3A==, + } + + "@vue/devtools-core@8.2.1": + resolution: + { + integrity: sha512-s/VfAY9oDTb/kFEWmy461jaFde2MIV1RO/gi1vwM+PAZBZ/Pc2Ndu3BNBdZUze8QDUuyYvElbEEGA83syjJfzA==, + } peerDependencies: vue: ^3.0.0 - '@vue/devtools-kit@8.2.1': - resolution: {integrity: sha512-FIGIuq3AWReEpbAHY/cRGeHDfI0qOb8OCQ3YjbEAX04uaxIDbGc9rhkbVcG7rnfHPXE3RsU5KrWOu9V/okd8AQ==} - - '@vue/devtools-shared@8.2.1': - resolution: {integrity: sha512-Fkac7lUdGReh6pVOi3AYPRGe82LQqRmAfThW7RRligOAP0ZA/Z1z9XLHDM9dv34pV2HRc79DK8uKPeG2fLnA/g==} - - '@vue/language-core@2.2.12': - resolution: {integrity: sha512-IsGljWbKGU1MZpBPN+BvPAdr55YPkj2nB/TBNGNC32Vy2qLG25DYu/NBN2vNtZqdRbTRjaoYrahLrToim2NanA==} + "@vue/devtools-kit@8.2.1": + resolution: + { + integrity: sha512-FIGIuq3AWReEpbAHY/cRGeHDfI0qOb8OCQ3YjbEAX04uaxIDbGc9rhkbVcG7rnfHPXE3RsU5KrWOu9V/okd8AQ==, + } + + "@vue/devtools-shared@8.2.1": + resolution: + { + integrity: sha512-Fkac7lUdGReh6pVOi3AYPRGe82LQqRmAfThW7RRligOAP0ZA/Z1z9XLHDM9dv34pV2HRc79DK8uKPeG2fLnA/g==, + } + + "@vue/language-core@2.2.12": + resolution: + { + integrity: sha512-IsGljWbKGU1MZpBPN+BvPAdr55YPkj2nB/TBNGNC32Vy2qLG25DYu/NBN2vNtZqdRbTRjaoYrahLrToim2NanA==, + } peerDependencies: - typescript: '*' + typescript: "*" peerDependenciesMeta: typescript: optional: true - '@vue/reactivity@3.5.41': - resolution: {integrity: sha512-rznsqKM0np0x18EjzF8x88MpEhdNsffbvFbckLL5+oUKz1BxAImEmO7J1ArRYSyo6aQaVoBDp7jEkT91OOxydA==} - - '@vue/runtime-core@3.5.41': - resolution: {integrity: sha512-Vcry58hiAKwGen9Z1jUZE0feFsNArPCMOImYI8el48A9Idf6DuQYD0U05zZIF2Iad1hGhPSvcbBbAOhNr55fhg==} - - '@vue/runtime-dom@3.5.41': - resolution: {integrity: sha512-3vVBahVBS9+U6cmXBLyb8nE6/yYo4J/CGI9eVFs3KiMc0YHuudwKyShTD65jtJy/L9PUUxNAFu4cj4LiJ0UFbw==} - - '@vue/server-renderer@3.5.41': - resolution: {integrity: sha512-n6hx/pNFfbD6SuyeuMVkvqox8bwf/ET9JlA/kAz/imw8sw++wkqKe2mHX5KutjPpbKE4Z56yTHszoOjGMI9igQ==} - - '@vue/shared@3.5.38': - resolution: {integrity: sha512-FTW0AFZNaK5/mOqvGBwVfUlNLU38TiQn4+DQgIFUnrBBJQ1crMJ82yeGQLV5jyKFsO8yRukpbuP7x+nRbH6aug==} - - '@vue/shared@3.5.40': - resolution: {integrity: sha512-WxnBtruIqOoV3rA4jeKDWzrYI5h7Cp4+pjwDi8kWGHz+IslhiN+wguLVVhtv2l8VoU02rzDCVfDjgCl1lNpZVg==} - - '@vue/shared@3.5.41': - resolution: {integrity: sha512-IOnwSCma8j+9xJT6b8H0dEYidC80NsYmNMlZxRsukYcSoGaDBohog5hDxzeUXdFeGWFA++vWvxqOmrr96VlqMA==} + "@vue/reactivity@3.5.41": + resolution: + { + integrity: sha512-rznsqKM0np0x18EjzF8x88MpEhdNsffbvFbckLL5+oUKz1BxAImEmO7J1ArRYSyo6aQaVoBDp7jEkT91OOxydA==, + } + + "@vue/runtime-core@3.5.41": + resolution: + { + integrity: sha512-Vcry58hiAKwGen9Z1jUZE0feFsNArPCMOImYI8el48A9Idf6DuQYD0U05zZIF2Iad1hGhPSvcbBbAOhNr55fhg==, + } + + "@vue/runtime-dom@3.5.41": + resolution: + { + integrity: sha512-3vVBahVBS9+U6cmXBLyb8nE6/yYo4J/CGI9eVFs3KiMc0YHuudwKyShTD65jtJy/L9PUUxNAFu4cj4LiJ0UFbw==, + } + + "@vue/server-renderer@3.5.41": + resolution: + { + integrity: sha512-n6hx/pNFfbD6SuyeuMVkvqox8bwf/ET9JlA/kAz/imw8sw++wkqKe2mHX5KutjPpbKE4Z56yTHszoOjGMI9igQ==, + } + + "@vue/shared@3.5.38": + resolution: + { + integrity: sha512-FTW0AFZNaK5/mOqvGBwVfUlNLU38TiQn4+DQgIFUnrBBJQ1crMJ82yeGQLV5jyKFsO8yRukpbuP7x+nRbH6aug==, + } + + "@vue/shared@3.5.40": + resolution: + { + integrity: sha512-WxnBtruIqOoV3rA4jeKDWzrYI5h7Cp4+pjwDi8kWGHz+IslhiN+wguLVVhtv2l8VoU02rzDCVfDjgCl1lNpZVg==, + } + + "@vue/shared@3.5.41": + resolution: + { + integrity: sha512-IOnwSCma8j+9xJT6b8H0dEYidC80NsYmNMlZxRsukYcSoGaDBohog5hDxzeUXdFeGWFA++vWvxqOmrr96VlqMA==, + } abbrev@3.0.1: - resolution: {integrity: sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg==} - engines: {node: ^18.17.0 || >=20.5.0} + resolution: + { + integrity: sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg==, + } + engines: { node: ^18.17.0 || >=20.5.0 } abort-controller@3.0.0: - resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} - engines: {node: '>=6.5'} + resolution: + { + integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==, + } + engines: { node: ">=6.5" } accepts@1.3.8: - resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} - engines: {node: '>= 0.6'} + resolution: + { + integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==, + } + engines: { node: ">= 0.6" } acorn-import-attributes@1.9.5: - resolution: {integrity: sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==} + resolution: + { + integrity: sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==, + } peerDependencies: acorn: ^8 acorn@8.16.0: - resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} - engines: {node: '>=0.4.0'} + resolution: + { + integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==, + } + engines: { node: ">=0.4.0" } hasBin: true acorn@8.18.0: - resolution: {integrity: sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==} - engines: {node: '>=0.4.0'} + resolution: + { + integrity: sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==, + } + engines: { node: ">=0.4.0" } hasBin: true agent-base@7.1.4: - resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} - engines: {node: '>= 14'} + resolution: + { + integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==, + } + engines: { node: ">= 14" } alien-signals@1.0.13: - resolution: {integrity: sha512-OGj9yyTnJEttvzhTUWuscOvtqxq5vrhF7vL9oS0xJ2mK0ItPYP1/y+vCFebfxoEyAz0++1AIwJ5CMr+Fk3nDmg==} + resolution: + { + integrity: sha512-OGj9yyTnJEttvzhTUWuscOvtqxq5vrhF7vL9oS0xJ2mK0ItPYP1/y+vCFebfxoEyAz0++1AIwJ5CMr+Fk3nDmg==, + } ansi-regex@5.0.1: - resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==, + } + engines: { node: ">=8" } ansi-regex@6.2.2: - resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==, + } + engines: { node: ">=12" } ansi-styles@4.3.0: - resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==, + } + engines: { node: ">=8" } ansi-styles@6.2.3: - resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==, + } + engines: { node: ">=12" } ansis@4.3.1: - resolution: {integrity: sha512-BJ8/l4R5LRE7hW9WdSuGYrLSHi2ynxeFpDFbH0K/CgNeY/tyhk+vO6TYxXC5r5CpUhNVX310xzPsN/H9lCdfOA==} - engines: {node: '>=14'} + resolution: + { + integrity: sha512-BJ8/l4R5LRE7hW9WdSuGYrLSHi2ynxeFpDFbH0K/CgNeY/tyhk+vO6TYxXC5r5CpUhNVX310xzPsN/H9lCdfOA==, + } + engines: { node: ">=14" } any-promise@1.3.0: - resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + resolution: + { + integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==, + } anymatch@3.1.3: - resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} - engines: {node: '>= 8'} + resolution: + { + integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==, + } + engines: { node: ">= 8" } archiver-utils@5.0.2: - resolution: {integrity: sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA==} - engines: {node: '>= 14'} + resolution: + { + integrity: sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA==, + } + engines: { node: ">= 14" } archiver@7.0.1: - resolution: {integrity: sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==} - engines: {node: '>= 14'} + resolution: + { + integrity: sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==, + } + engines: { node: ">= 14" } arg@5.0.2: - resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} + resolution: + { + integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==, + } argparse@2.0.1: - resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + resolution: + { + integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==, + } ast-kit@2.2.0: - resolution: {integrity: sha512-m1Q/RaVOnTp9JxPX+F+Zn7IcLYMzM8kZofDImfsKZd8MbR+ikdOzTeztStWqfrqIxZnYWryyI9ePm3NGjnZgGw==} - engines: {node: '>=20.19.0'} + resolution: + { + integrity: sha512-m1Q/RaVOnTp9JxPX+F+Zn7IcLYMzM8kZofDImfsKZd8MbR+ikdOzTeztStWqfrqIxZnYWryyI9ePm3NGjnZgGw==, + } + engines: { node: ">=20.19.0" } ast-walker-scope@0.9.0: - resolution: {integrity: sha512-IJdzo2vLiElBxKzwS36VsCue/62d6IdWjnPB2v3nuPKeWGynp6FF/CYoLa5i/3jXH/z97ZDdsXz6abpgM6w07A==} - engines: {node: '>=20.19.0'} + resolution: + { + integrity: sha512-IJdzo2vLiElBxKzwS36VsCue/62d6IdWjnPB2v3nuPKeWGynp6FF/CYoLa5i/3jXH/z97ZDdsXz6abpgM6w07A==, + } + engines: { node: ">=20.19.0" } async-sema@3.1.1: - resolution: {integrity: sha512-tLRNUXati5MFePdAk8dw7Qt7DpxPB60ofAgn8WRhW6a2rcimZnYBP9oxHiv0OHy+Wz7kPMG+t4LGdt31+4EmGg==} + resolution: + { + integrity: sha512-tLRNUXati5MFePdAk8dw7Qt7DpxPB60ofAgn8WRhW6a2rcimZnYBP9oxHiv0OHy+Wz7kPMG+t4LGdt31+4EmGg==, + } async@3.2.6: - resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} + resolution: + { + integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==, + } at-least-node@1.0.0: - resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==} - engines: {node: '>= 4.0.0'} + resolution: + { + integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==, + } + engines: { node: ">= 4.0.0" } autoprefixer@10.5.0: - resolution: {integrity: sha512-FMhOoZV4+qR6aTUALKX2rEqGG+oyATvwBt9IIzVR5rMa2HRWPkxf+P+PAJLD1I/H5/II+HuZcBJYEFBpq39ong==} - engines: {node: ^10 || ^12 || >=14} + resolution: + { + integrity: sha512-FMhOoZV4+qR6aTUALKX2rEqGG+oyATvwBt9IIzVR5rMa2HRWPkxf+P+PAJLD1I/H5/II+HuZcBJYEFBpq39ong==, + } + engines: { node: ^10 || ^12 || >=14 } hasBin: true peerDependencies: postcss: ^8.1.0 autoprefixer@10.5.4: - resolution: {integrity: sha512-MaU0U/za7N3r6brxD4YB/l4NSrFzLPlANv6wEuQVaIPlD3L4W9rFcQPbL/EilY9BHhHvhfcz3gInDLrEtWT4EA==} - engines: {node: ^10 || ^12 || >=14} + resolution: + { + integrity: sha512-MaU0U/za7N3r6brxD4YB/l4NSrFzLPlANv6wEuQVaIPlD3L4W9rFcQPbL/EilY9BHhHvhfcz3gInDLrEtWT4EA==, + } + engines: { node: ^10 || ^12 || >=14 } hasBin: true peerDependencies: postcss: ^8.1.0 b4a@1.8.1: - resolution: {integrity: sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==} + resolution: + { + integrity: sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==, + } peerDependencies: - react-native-b4a: '*' + react-native-b4a: "*" peerDependenciesMeta: react-native-b4a: optional: true balanced-match@1.0.2: - resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + resolution: + { + integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==, + } balanced-match@4.0.4: - resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} - engines: {node: 18 || 20 || >=22} + resolution: + { + integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==, + } + engines: { node: 18 || 20 || >=22 } bare-events@2.9.1: - resolution: {integrity: sha512-Z0oHEHAFDZkffN8Qc39zNZjQlMDkPJRyyyZieU1VH7u8c5S+qHZ2S8ixdKIAxEjfHO7FJxXmJWgteOghVanIsg==} + resolution: + { + integrity: sha512-Z0oHEHAFDZkffN8Qc39zNZjQlMDkPJRyyyZieU1VH7u8c5S+qHZ2S8ixdKIAxEjfHO7FJxXmJWgteOghVanIsg==, + } peerDependencies: - bare-abort-controller: '*' + bare-abort-controller: "*" peerDependenciesMeta: bare-abort-controller: optional: true bare-fs@4.8.0: - resolution: {integrity: sha512-fM+MhCvdQhZ7NV6S95a07gPSqjIYKn6mFaXfx266wN3ajZGl/+1AzH+ubkXQ0fFZvOe2nk9VHkzdYkQE5zMV3Q==} - engines: {bare: '>=1.28.0'} + resolution: + { + integrity: sha512-fM+MhCvdQhZ7NV6S95a07gPSqjIYKn6mFaXfx266wN3ajZGl/+1AzH+ubkXQ0fFZvOe2nk9VHkzdYkQE5zMV3Q==, + } + engines: { bare: ">=1.28.0" } peerDependencies: - bare-buffer: '*' + bare-buffer: "*" peerDependenciesMeta: bare-buffer: optional: true bare-path@3.1.1: - resolution: {integrity: sha512-JprUlveX3QjApC1cTpsUOiscADftCGVWkzitbHsRqv84hzYwYHw2mbluddsq5TvI8mH/8Ov1f4BiMAdcB0oYnQ==} + resolution: + { + integrity: sha512-JprUlveX3QjApC1cTpsUOiscADftCGVWkzitbHsRqv84hzYwYHw2mbluddsq5TvI8mH/8Ov1f4BiMAdcB0oYnQ==, + } bare-stream@2.13.3: - resolution: {integrity: sha512-Kc+brLqvEqGkjyfiwJmImAOqLZL7OsoLKuavx+hJjgVV3nLTOjloJyPMFxjUPerGGHrNH0fLU06jjykMLWrERQ==} + resolution: + { + integrity: sha512-Kc+brLqvEqGkjyfiwJmImAOqLZL7OsoLKuavx+hJjgVV3nLTOjloJyPMFxjUPerGGHrNH0fLU06jjykMLWrERQ==, + } peerDependencies: - bare-abort-controller: '*' - bare-buffer: '*' - bare-events: '*' + bare-abort-controller: "*" + bare-buffer: "*" + bare-events: "*" peerDependenciesMeta: bare-abort-controller: optional: true @@ -1440,328 +2218,574 @@ packages: optional: true bare-url@2.5.1: - resolution: {integrity: sha512-cD5ciQuKlx+eumTCfqbfiL+fhQm+dHbVNB/cX4+d+I/nx4JsVop9VoFEPAs5RJ6I84QR6bZIBjpd2kjDOYeWcg==} + resolution: + { + integrity: sha512-cD5ciQuKlx+eumTCfqbfiL+fhQm+dHbVNB/cX4+d+I/nx4JsVop9VoFEPAs5RJ6I84QR6bZIBjpd2kjDOYeWcg==, + } base64-js@1.5.1: - resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + resolution: + { + integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==, + } baseline-browser-mapping@2.10.31: - resolution: {integrity: sha512-MujYO3eP72uvmSE0i4wltsodRfIpZATP3jvzRNRGGxgzId7aVocVJJV3nf01qnzzKFGxQVC9bpWxl5cjxTr/7Q==} - engines: {node: '>=6.0.0'} + resolution: + { + integrity: sha512-MujYO3eP72uvmSE0i4wltsodRfIpZATP3jvzRNRGGxgzId7aVocVJJV3nf01qnzzKFGxQVC9bpWxl5cjxTr/7Q==, + } + engines: { node: ">=6.0.0" } hasBin: true baseline-browser-mapping@2.11.12: - resolution: {integrity: sha512-r7WnVImvVCeFpf2DOXfy41aPWzeNg3H/A2X4dKmy1QL0MSyyk/e7z8ihJ3N6Nn2PsdhkVlqnEfnUE4a05P2aTA==} - engines: {node: '>=6.0.0'} + resolution: + { + integrity: sha512-r7WnVImvVCeFpf2DOXfy41aPWzeNg3H/A2X4dKmy1QL0MSyyk/e7z8ihJ3N6Nn2PsdhkVlqnEfnUE4a05P2aTA==, + } + engines: { node: ">=6.0.0" } hasBin: true binary-extensions@2.3.0: - resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==, + } + engines: { node: ">=8" } bindings@1.5.0: - resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} + resolution: + { + integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==, + } birpc@2.9.0: - resolution: {integrity: sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==} + resolution: + { + integrity: sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==, + } birpc@4.0.0: - resolution: {integrity: sha512-LShSxJP0KTmd101b6DRyGBj57LZxSDYWKitQNW/mi8GRMvZb078Uf9+pveax1DrVL89vm7mWe+TovdI/UDOuPw==} + resolution: + { + integrity: sha512-LShSxJP0KTmd101b6DRyGBj57LZxSDYWKitQNW/mi8GRMvZb078Uf9+pveax1DrVL89vm7mWe+TovdI/UDOuPw==, + } boolbase@1.0.0: - resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + resolution: + { + integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==, + } brace-expansion@1.1.12: - resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} + resolution: + { + integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==, + } brace-expansion@2.1.0: - resolution: {integrity: sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==} + resolution: + { + integrity: sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==, + } brace-expansion@2.1.4: - resolution: {integrity: sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==} + resolution: + { + integrity: sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==, + } brace-expansion@5.0.9: - resolution: {integrity: sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==} - engines: {node: 20 || >=22} + resolution: + { + integrity: sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==, + } + engines: { node: 20 || >=22 } braces@3.0.3: - resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==, + } + engines: { node: ">=8" } browserslist@4.28.2: - resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==} - engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + resolution: + { + integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==, + } + engines: { node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7 } hasBin: true browserslist@4.28.7: - resolution: {integrity: sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw==} - engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + resolution: + { + integrity: sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw==, + } + engines: { node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7 } hasBin: true buffer-builder@0.2.0: - resolution: {integrity: sha512-7VPMEPuYznPSoR21NE1zvd2Xna6c/CloiZCfcMXR1Jny6PjX0N4Nsa38zcBFo/FMK+BlA+FLKbJCQ0i2yxp+Xg==} + resolution: + { + integrity: sha512-7VPMEPuYznPSoR21NE1zvd2Xna6c/CloiZCfcMXR1Jny6PjX0N4Nsa38zcBFo/FMK+BlA+FLKbJCQ0i2yxp+Xg==, + } buffer-crc32@1.0.0: - resolution: {integrity: sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==} - engines: {node: '>=8.0.0'} + resolution: + { + integrity: sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==, + } + engines: { node: ">=8.0.0" } buffer-from@1.1.2: - resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + resolution: + { + integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==, + } buffer@6.0.3: - resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + resolution: + { + integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==, + } bundle-name@4.1.0: - resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==, + } + engines: { node: ">=18" } c12@3.3.4: - resolution: {integrity: sha512-cM0ApFQSBXuourJejzwv/AuPRvAxordTyParRVcHjjtXirtkzM0uK2L9TTn9s0cXZbG7E55jCivRQzoxYmRAlA==} + resolution: + { + integrity: sha512-cM0ApFQSBXuourJejzwv/AuPRvAxordTyParRVcHjjtXirtkzM0uK2L9TTn9s0cXZbG7E55jCivRQzoxYmRAlA==, + } peerDependencies: - magicast: '*' + magicast: "*" peerDependenciesMeta: magicast: optional: true cac@6.7.14: - resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==, + } + engines: { node: ">=8" } cac@7.0.0: - resolution: {integrity: sha512-tixWYgm5ZoOD+3g6UTea91eow5z6AAHaho3g0V9CNSNb45gM8SmflpAc+GRd1InC4AqN/07Unrgp56Y94N9hJQ==} - engines: {node: '>=20.19.0'} + resolution: + { + integrity: sha512-tixWYgm5ZoOD+3g6UTea91eow5z6AAHaho3g0V9CNSNb45gM8SmflpAc+GRd1InC4AqN/07Unrgp56Y94N9hJQ==, + } + engines: { node: ">=20.19.0" } cache-content-type@1.0.1: - resolution: {integrity: sha512-IKufZ1o4Ut42YUrZSo8+qnMTrFuKkvyoLXUywKz9GJ5BrhOFGhLdkx9sG4KAnVvbY6kEcSFjLQul+DVmBm2bgA==} - engines: {node: '>= 6.0.0'} + resolution: + { + integrity: sha512-IKufZ1o4Ut42YUrZSo8+qnMTrFuKkvyoLXUywKz9GJ5BrhOFGhLdkx9sG4KAnVvbY6kEcSFjLQul+DVmBm2bgA==, + } + engines: { node: ">= 6.0.0" } call-bind-apply-helpers@1.0.2: - resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==, + } + engines: { node: ">= 0.4" } call-bound@1.0.4: - resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==, + } + engines: { node: ">= 0.4" } camelcase-css@2.0.1: - resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} - engines: {node: '>= 6'} + resolution: + { + integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==, + } + engines: { node: ">= 6" } caniuse-api@4.0.0: - resolution: {integrity: sha512-B0hQ1OLyJuHTQSOWXvwibWqM6DCoqJdvBA6X1S/53bd4XU7LJ1yurIPlrsouol3mw1jh9pGI4ivubSpmJeIqCA==} + resolution: + { + integrity: sha512-B0hQ1OLyJuHTQSOWXvwibWqM6DCoqJdvBA6X1S/53bd4XU7LJ1yurIPlrsouol3mw1jh9pGI4ivubSpmJeIqCA==, + } caniuse-lite@1.0.30001793: - resolution: {integrity: sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==} + resolution: + { + integrity: sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==, + } caniuse-lite@1.0.30001809: - resolution: {integrity: sha512-xxWVywk6a6Arlk+hymeycyn/VgqEfLDxupvhH/xiY5SJ/18kmi9o6MiO320DCUzypORHLtvh0I4i04tUhCNHNQ==} + resolution: + { + integrity: sha512-xxWVywk6a6Arlk+hymeycyn/VgqEfLDxupvhH/xiY5SJ/18kmi9o6MiO320DCUzypORHLtvh0I4i04tUhCNHNQ==, + } chalk@4.1.2: - resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} - engines: {node: '>=10'} + resolution: + { + integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==, + } + engines: { node: ">=10" } character-entities@2.0.2: - resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==} + resolution: + { + integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==, + } chokidar@3.6.0: - resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} - engines: {node: '>= 8.10.0'} + resolution: + { + integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==, + } + engines: { node: ">= 8.10.0" } chokidar@4.0.3: - resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} - engines: {node: '>= 14.16.0'} + resolution: + { + integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==, + } + engines: { node: ">= 14.16.0" } chokidar@5.0.0: - resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} - engines: {node: '>= 20.19.0'} + resolution: + { + integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==, + } + engines: { node: ">= 20.19.0" } chownr@3.0.0: - resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==, + } + engines: { node: ">=18" } citty@0.1.6: - resolution: {integrity: sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==} + resolution: + { + integrity: sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==, + } citty@0.2.2: - resolution: {integrity: sha512-+6vJA3L98yv+IdfKGZHBNiGW5KHn22e/JwID0Strsz8h4S/csAu/OuICwxrg44k5MRiZHWIo8XXuJgQTriRP4w==} + resolution: + { + integrity: sha512-+6vJA3L98yv+IdfKGZHBNiGW5KHn22e/JwID0Strsz8h4S/csAu/OuICwxrg44k5MRiZHWIo8XXuJgQTriRP4w==, + } cliui@8.0.1: - resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==, + } + engines: { node: ">=12" } cliui@9.0.1: - resolution: {integrity: sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==} - engines: {node: '>=20'} + resolution: + { + integrity: sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==, + } + engines: { node: ">=20" } cluster-key-slot@1.1.1: - resolution: {integrity: sha512-rwHwUfXL40Chm1r08yrhU3qpUvdVlgkKNeyeGPOxnW8/SyVDvgRaed/Uz54AqWNaTCAThlj6QAs3TZcKI0xDEw==} - engines: {node: '>=0.10.0'} + resolution: + { + integrity: sha512-rwHwUfXL40Chm1r08yrhU3qpUvdVlgkKNeyeGPOxnW8/SyVDvgRaed/Uz54AqWNaTCAThlj6QAs3TZcKI0xDEw==, + } + engines: { node: ">=0.10.0" } co@4.6.0: - resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==} - engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} + resolution: + { + integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==, + } + engines: { iojs: ">= 1.0.0", node: ">= 0.12.0" } color-convert@2.0.1: - resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} - engines: {node: '>=7.0.0'} + resolution: + { + integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==, + } + engines: { node: ">=7.0.0" } color-name@1.1.4: - resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + resolution: + { + integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==, + } colorjs.io@0.5.2: - resolution: {integrity: sha512-twmVoizEW7ylZSN32OgKdXRmo1qg+wT5/6C3xu5b9QsWzSFAhHLn2xd8ro0diCsKfCj1RdaTP/nrcW+vAoQPIw==} + resolution: + { + integrity: sha512-twmVoizEW7ylZSN32OgKdXRmo1qg+wT5/6C3xu5b9QsWzSFAhHLn2xd8ro0diCsKfCj1RdaTP/nrcW+vAoQPIw==, + } commander@11.1.0: - resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==} - engines: {node: '>=16'} + resolution: + { + integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==, + } + engines: { node: ">=16" } commander@2.20.3: - resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} + resolution: + { + integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==, + } commander@4.1.1: - resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} - engines: {node: '>= 6'} + resolution: + { + integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==, + } + engines: { node: ">= 6" } commander@6.2.1: - resolution: {integrity: sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==} - engines: {node: '>= 6'} + resolution: + { + integrity: sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==, + } + engines: { node: ">= 6" } commondir@1.0.1: - resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} + resolution: + { + integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==, + } compatx@0.2.0: - resolution: {integrity: sha512-6gLRNt4ygsi5NyMVhceOCFv14CIdDFN7fQjX1U4+47qVE/+kjPoXMK65KWK+dWxmFzMTuKazoQ9sch6pM0p5oA==} + resolution: + { + integrity: sha512-6gLRNt4ygsi5NyMVhceOCFv14CIdDFN7fQjX1U4+47qVE/+kjPoXMK65KWK+dWxmFzMTuKazoQ9sch6pM0p5oA==, + } compress-commons@6.0.2: - resolution: {integrity: sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==} - engines: {node: '>= 14'} + resolution: + { + integrity: sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==, + } + engines: { node: ">= 14" } concat-map@0.0.1: - resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + resolution: + { + integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==, + } confbox@0.1.8: - resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} + resolution: + { + integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==, + } confbox@0.2.4: - resolution: {integrity: sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==} + resolution: + { + integrity: sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==, + } consola@3.4.2: - resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} - engines: {node: ^14.18.0 || >=16.10.0} + resolution: + { + integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==, + } + engines: { node: ^14.18.0 || >=16.10.0 } content-disposition@0.5.4: - resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} - engines: {node: '>= 0.6'} + resolution: + { + integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==, + } + engines: { node: ">= 0.6" } content-type@1.0.5: - resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} - engines: {node: '>= 0.6'} + resolution: + { + integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==, + } + engines: { node: ">= 0.6" } convert-source-map@2.0.0: - resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + resolution: + { + integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==, + } cookie-es@1.2.3: - resolution: {integrity: sha512-lXVyvUvrNXblMqzIRrxHb57UUVmqsSWlxqt3XIjCkUP0wDAf6uicO6KMbEgYrMNtEvWgWHwe42CKxPu9MYAnWw==} + resolution: + { + integrity: sha512-lXVyvUvrNXblMqzIRrxHb57UUVmqsSWlxqt3XIjCkUP0wDAf6uicO6KMbEgYrMNtEvWgWHwe42CKxPu9MYAnWw==, + } cookie-es@2.0.1: - resolution: {integrity: sha512-aVf4A4hI2w70LnF7GG+7xDQUkliwiXWXFvTjkip4+b64ygDQ2sJPRSKFDHbxn8o0xu9QzPkMuuiWIXyFSE2slA==} + resolution: + { + integrity: sha512-aVf4A4hI2w70LnF7GG+7xDQUkliwiXWXFvTjkip4+b64ygDQ2sJPRSKFDHbxn8o0xu9QzPkMuuiWIXyFSE2slA==, + } cookie-es@3.1.1: - resolution: {integrity: sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg==} + resolution: + { + integrity: sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg==, + } cookies@0.9.1: - resolution: {integrity: sha512-TG2hpqe4ELx54QER/S3HQ9SRVnQnGBtKUz5bLQWtYAQ+o6GpgMs6sYUvaiJjVxb+UXwhRhAEP3m7LbsIZ77Hmw==} - engines: {node: '>= 0.8'} + resolution: + { + integrity: sha512-TG2hpqe4ELx54QER/S3HQ9SRVnQnGBtKUz5bLQWtYAQ+o6GpgMs6sYUvaiJjVxb+UXwhRhAEP3m7LbsIZ77Hmw==, + } + engines: { node: ">= 0.8" } core-util-is@1.0.3: - resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + resolution: + { + integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==, + } crc-32@1.2.2: - resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==} - engines: {node: '>=0.8'} + resolution: + { + integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==, + } + engines: { node: ">=0.8" } hasBin: true crc32-stream@6.0.0: - resolution: {integrity: sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==} - engines: {node: '>= 14'} + resolution: + { + integrity: sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==, + } + engines: { node: ">= 14" } croner@10.0.1: - resolution: {integrity: sha512-ixNtAJndqh173VQ4KodSdJEI6nuioBWI0V1ITNKhZZsO0pEMoDxz539T4FTTbSZ/xIOSuDnzxLVRqBVSvPNE2g==} - engines: {node: '>=18.0'} + resolution: + { + integrity: sha512-ixNtAJndqh173VQ4KodSdJEI6nuioBWI0V1ITNKhZZsO0pEMoDxz539T4FTTbSZ/xIOSuDnzxLVRqBVSvPNE2g==, + } + engines: { node: ">=18.0" } cross-spawn@7.0.6: - resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} - engines: {node: '>= 8'} + resolution: + { + integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==, + } + engines: { node: ">= 8" } crossws@0.3.5: - resolution: {integrity: sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA==} + resolution: + { + integrity: sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA==, + } crossws@0.4.10: - resolution: {integrity: sha512-pz3oubH/dt12KjqsUB0IuXW4nwRDQ583iDsP4555Cpdqx0NoU7pGlWBcayyFI8f/l/idRpgjMEfwuOxSWJYlIA==} + resolution: + { + integrity: sha512-pz3oubH/dt12KjqsUB0IuXW4nwRDQ583iDsP4555Cpdqx0NoU7pGlWBcayyFI8f/l/idRpgjMEfwuOxSWJYlIA==, + } peerDependencies: - srvx: '>=0.11.5' + srvx: ">=0.11.5" peerDependenciesMeta: srvx: optional: true css-select@5.2.2: - resolution: {integrity: sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==} + resolution: + { + integrity: sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==, + } css-tree@2.2.1: - resolution: {integrity: sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==} - engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'} + resolution: + { + integrity: sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==, + } + engines: { node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: ">=7.0.0" } css-tree@3.2.1: - resolution: {integrity: sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==} - engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + resolution: + { + integrity: sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==, + } + engines: { node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0 } css-what@6.2.2: - resolution: {integrity: sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==} - engines: {node: '>= 6'} + resolution: + { + integrity: sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==, + } + engines: { node: ">= 6" } cssesc@3.0.0: - resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} - engines: {node: '>=4'} + resolution: + { + integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==, + } + engines: { node: ">=4" } hasBin: true cssnano-preset-default@8.0.4: - resolution: {integrity: sha512-WUn2NmdLD0FlUI8XdQrlfDjVNGLOI3cxaw7Y6msbfxn4G2vJByuO6M73Wo5B9Rd0Ap36SQhG7wsOXAMnVdC2eQ==} - engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} + resolution: + { + integrity: sha512-WUn2NmdLD0FlUI8XdQrlfDjVNGLOI3cxaw7Y6msbfxn4G2vJByuO6M73Wo5B9Rd0Ap36SQhG7wsOXAMnVdC2eQ==, + } + engines: { node: ^22.11.0 || ^24.11.0 || >=26.0 } peerDependencies: postcss: ^8.5.25 cssnano-utils@6.0.2: - resolution: {integrity: sha512-AqysikWP69dOKAP/UMvUYrlZ1gvvQu0/eMFVUKLhH+ZM23dUAKXC31xKvNyL9It2UMF2uDmK4XotBeO9vSBKzg==} - engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} + resolution: + { + integrity: sha512-AqysikWP69dOKAP/UMvUYrlZ1gvvQu0/eMFVUKLhH+ZM23dUAKXC31xKvNyL9It2UMF2uDmK4XotBeO9vSBKzg==, + } + engines: { node: ^22.11.0 || ^24.11.0 || >=26.0 } peerDependencies: postcss: ^8.5.25 cssnano@8.0.4: - resolution: {integrity: sha512-m6epEvKkzysdXamdK4BU9S6lQzx+syY+5Qm4f2dbpxlewBtEYOKi2v2iIr1jLsjuAr3V1WjRp6hXFPH1BEDirw==} - engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} + resolution: + { + integrity: sha512-m6epEvKkzysdXamdK4BU9S6lQzx+syY+5Qm4f2dbpxlewBtEYOKi2v2iIr1jLsjuAr3V1WjRp6hXFPH1BEDirw==, + } + engines: { node: ^22.11.0 || ^24.11.0 || >=26.0 } peerDependencies: postcss: ^8.5.25 csso@5.0.5: - resolution: {integrity: sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==} - engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'} + resolution: + { + integrity: sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==, + } + engines: { node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: ">=7.0.0" } csstype@3.2.3: - resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + resolution: + { + integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==, + } db0@0.3.4: - resolution: {integrity: sha512-RiXXi4WaNzPTHEOu8UPQKMooIbqOEyqA1t7Z6MsdxSCeb8iUC9ko3LcmsLmeUt2SM5bctfArZKkRQggKZz7JNw==} + resolution: + { + integrity: sha512-RiXXi4WaNzPTHEOu8UPQKMooIbqOEyqA1t7Z6MsdxSCeb8iUC9ko3LcmsLmeUt2SM5bctfArZKkRQggKZz7JNw==, + } peerDependencies: - '@electric-sql/pglite': '*' - '@libsql/client': '*' - better-sqlite3: '*' - drizzle-orm: '*' - mysql2: '*' - sqlite3: '*' + "@electric-sql/pglite": "*" + "@libsql/client": "*" + better-sqlite3: "*" + drizzle-orm: "*" + mysql2: "*" + sqlite3: "*" peerDependenciesMeta: - '@electric-sql/pglite': + "@electric-sql/pglite": optional: true - '@libsql/client': + "@libsql/client": optional: true better-sqlite3: optional: true @@ -1773,255 +2797,462 @@ packages: optional: true de-indent@1.0.2: - resolution: {integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==} + resolution: + { + integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==, + } debug@3.2.7: - resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} + resolution: + { + integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==, + } peerDependencies: - supports-color: '*' + supports-color: "*" peerDependenciesMeta: supports-color: optional: true debug@4.4.3: - resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} - engines: {node: '>=6.0'} + resolution: + { + integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==, + } + engines: { node: ">=6.0" } peerDependencies: - supports-color: '*' + supports-color: "*" peerDependenciesMeta: supports-color: optional: true decode-named-character-reference@1.2.0: - resolution: {integrity: sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==} + resolution: + { + integrity: sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==, + } deep-equal@1.0.1: - resolution: {integrity: sha512-bHtC0iYvWhyaTzvV3CZgPeZQqCOBGyGsVV7v4eevpdkLHfiSrXUdBG+qAuSz4RI70sszvjQ1QSZ98An1yNwpSw==} + resolution: + { + integrity: sha512-bHtC0iYvWhyaTzvV3CZgPeZQqCOBGyGsVV7v4eevpdkLHfiSrXUdBG+qAuSz4RI70sszvjQ1QSZ98An1yNwpSw==, + } deepmerge@4.3.1: - resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} - engines: {node: '>=0.10.0'} + resolution: + { + integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==, + } + engines: { node: ">=0.10.0" } default-browser-id@5.0.1: - resolution: {integrity: sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==, + } + engines: { node: ">=18" } default-browser@5.5.0: - resolution: {integrity: sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==, + } + engines: { node: ">=18" } define-lazy-prop@3.0.0: - resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==, + } + engines: { node: ">=12" } defu@6.1.7: - resolution: {integrity: sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==} + resolution: + { + integrity: sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==, + } delegates@1.0.0: - resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==} + resolution: + { + integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==, + } denque@2.1.0: - resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} - engines: {node: '>=0.10'} + resolution: + { + integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==, + } + engines: { node: ">=0.10" } depd@1.1.2: - resolution: {integrity: sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==} - engines: {node: '>= 0.6'} + resolution: + { + integrity: sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==, + } + engines: { node: ">= 0.6" } depd@2.0.0: - resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} - engines: {node: '>= 0.8'} + resolution: + { + integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==, + } + engines: { node: ">= 0.8" } dequal@2.0.3: - resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} - engines: {node: '>=6'} + resolution: + { + integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==, + } + engines: { node: ">=6" } destr@2.0.5: - resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==} + resolution: + { + integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==, + } destroy@1.2.0: - resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} - engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + resolution: + { + integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==, + } + engines: { node: ">= 0.8", npm: 1.2.8000 || >= 1.4.16 } detect-libc@2.1.2: - resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==, + } + engines: { node: ">=8" } devalue@5.9.0: - resolution: {integrity: sha512-RWrqdArjvPbsATEhOPUo6Wndc/iWnkWKlhIrdlF3zMMYo/c3CVtoaVAyLtWxz5h8nSlkHzxnzV2uLydPXmtF+A==} + resolution: + { + integrity: sha512-RWrqdArjvPbsATEhOPUo6Wndc/iWnkWKlhIrdlF3zMMYo/c3CVtoaVAyLtWxz5h8nSlkHzxnzV2uLydPXmtF+A==, + } devlop@1.1.0: - resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + resolution: + { + integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==, + } didyoumean@1.2.2: - resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} + resolution: + { + integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==, + } diff@8.0.4: - resolution: {integrity: sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==} - engines: {node: '>=0.3.1'} + resolution: + { + integrity: sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==, + } + engines: { node: ">=0.3.1" } dlv@1.1.3: - resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} + resolution: + { + integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==, + } dom-serializer@2.0.0: - resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} + resolution: + { + integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==, + } domelementtype@2.3.0: - resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + resolution: + { + integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==, + } domhandler@5.0.3: - resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} - engines: {node: '>= 4'} + resolution: + { + integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==, + } + engines: { node: ">= 4" } domutils@3.2.2: - resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} + resolution: + { + integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==, + } dot-prop@10.2.0: - resolution: {integrity: sha512-BTJ9aZYL3vCfZlZOBLy9v8TUqWGQ0pzFnygKwFZt5udj6viBoFIBviKPUoZLDCPn1FoXffv6McQFDenrm5Krfw==} - engines: {node: '>=20'} + resolution: + { + integrity: sha512-BTJ9aZYL3vCfZlZOBLy9v8TUqWGQ0pzFnygKwFZt5udj6viBoFIBviKPUoZLDCPn1FoXffv6McQFDenrm5Krfw==, + } + engines: { node: ">=20" } dotenv@17.4.2: - resolution: {integrity: sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==, + } + engines: { node: ">=12" } dunder-proto@1.0.1: - resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==, + } + engines: { node: ">= 0.4" } duplexer@0.1.2: - resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==} + resolution: + { + integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==, + } eastasianwidth@0.2.0: - resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + resolution: + { + integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==, + } ee-first@1.1.1: - resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + resolution: + { + integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==, + } electron-to-chromium@1.5.359: - resolution: {integrity: sha512-8lPELWuYZIWk7NDvCNthtmMw/7Q5Wu25NpM4djFMHBmk8DubPAtL4YTOp7ou0e7HyJtwkVlWv8XMLURnrtgJQw==} + resolution: + { + integrity: sha512-8lPELWuYZIWk7NDvCNthtmMw/7Q5Wu25NpM4djFMHBmk8DubPAtL4YTOp7ou0e7HyJtwkVlWv8XMLURnrtgJQw==, + } electron-to-chromium@1.5.402: - resolution: {integrity: sha512-/oOpMaPT6Yg+6/1XQhyIPlzgj7Ye9zf+nNM2Uh6OcE2G2oNptWazFa+qB2Pdqqbsc9KnIDzgAntoYN0dbwOXwA==} + resolution: + { + integrity: sha512-/oOpMaPT6Yg+6/1XQhyIPlzgj7Ye9zf+nNM2Uh6OcE2G2oNptWazFa+qB2Pdqqbsc9KnIDzgAntoYN0dbwOXwA==, + } emoji-regex@10.6.0: - resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} + resolution: + { + integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==, + } emoji-regex@8.0.0: - resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + resolution: + { + integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==, + } emoji-regex@9.2.2: - resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + resolution: + { + integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==, + } encodeurl@1.0.2: - resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} - engines: {node: '>= 0.8'} + resolution: + { + integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==, + } + engines: { node: ">= 0.8" } encodeurl@2.0.0: - resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} - engines: {node: '>= 0.8'} + resolution: + { + integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==, + } + engines: { node: ">= 0.8" } entities@4.5.0: - resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} - engines: {node: '>=0.12'} + resolution: + { + integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==, + } + engines: { node: ">=0.12" } entities@7.0.1: - resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} - engines: {node: '>=0.12'} + resolution: + { + integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==, + } + engines: { node: ">=0.12" } error-stack-parser-es@1.0.5: - resolution: {integrity: sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==} + resolution: + { + integrity: sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==, + } error-stack-parser-es@2.0.1: - resolution: {integrity: sha512-J36ntO+rMQVRuR/umlmxmfLi4TpWwtmTnHoJoXTiC2xDNazs0VDPKcX6pbhZMDd2HFtH9isMBJXMdbki+A++Pg==} + resolution: + { + integrity: sha512-J36ntO+rMQVRuR/umlmxmfLi4TpWwtmTnHoJoXTiC2xDNazs0VDPKcX6pbhZMDd2HFtH9isMBJXMdbki+A++Pg==, + } errx@0.1.2: - resolution: {integrity: sha512-chfpPHmCerdo/rXr/nNvPZRkV4WwDRwzwnsJ0Uzz3tVi8Z41tDctRjduYy1138ii77AFlts1qvWtX3g/Acg91Q==} + resolution: + { + integrity: sha512-chfpPHmCerdo/rXr/nNvPZRkV4WwDRwzwnsJ0Uzz3tVi8Z41tDctRjduYy1138ii77AFlts1qvWtX3g/Acg91Q==, + } es-define-property@1.0.1: - resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==, + } + engines: { node: ">= 0.4" } es-errors@1.3.0: - resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==, + } + engines: { node: ">= 0.4" } es-module-lexer@2.3.1: - resolution: {integrity: sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==} + resolution: + { + integrity: sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==, + } es-object-atoms@1.1.1: - resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==, + } + engines: { node: ">= 0.4" } esbuild@0.28.1: - resolution: {integrity: sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==, + } + engines: { node: ">=18" } hasBin: true escalade@3.2.0: - resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} - engines: {node: '>=6'} + resolution: + { + integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==, + } + engines: { node: ">=6" } escape-html@1.0.3: - resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + resolution: + { + integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==, + } escape-string-regexp@5.0.0: - resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==, + } + engines: { node: ">=12" } estree-walker@2.0.2: - resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + resolution: + { + integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==, + } estree-walker@3.0.3: - resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + resolution: + { + integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==, + } etag@1.8.1: - resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} - engines: {node: '>= 0.6'} + resolution: + { + integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==, + } + engines: { node: ">= 0.6" } event-target-shim@5.0.1: - resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} - engines: {node: '>=6'} + resolution: + { + integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==, + } + engines: { node: ">=6" } events-universal@1.0.1: - resolution: {integrity: sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==} + resolution: + { + integrity: sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==, + } events@3.3.0: - resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} - engines: {node: '>=0.8.x'} + resolution: + { + integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==, + } + engines: { node: ">=0.8.x" } execa@8.0.1: - resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} - engines: {node: '>=16.17'} + resolution: + { + integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==, + } + engines: { node: ">=16.17" } exsolve@1.1.1: - resolution: {integrity: sha512-9U/jZUgjnSGyntRr6y5Muu1MJcwFl6kPu7k8qLF0IMNfLqvw0NZ4nnVDq0RVoZ0RvCyumib4Ez3KYrVfilrw+g==} + resolution: + { + integrity: sha512-9U/jZUgjnSGyntRr6y5Muu1MJcwFl6kPu7k8qLF0IMNfLqvw0NZ4nnVDq0RVoZ0RvCyumib4Ez3KYrVfilrw+g==, + } fast-fifo@1.3.2: - resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==} + resolution: + { + integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==, + } fast-glob@3.3.3: - resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} - engines: {node: '>=8.6.0'} + resolution: + { + integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==, + } + engines: { node: ">=8.6.0" } fast-npm-meta@2.2.0: - resolution: {integrity: sha512-99jPl8JkCSCa4VlboNU1XuL98ijm74Pm9CGo6H4BoMVoVh1uhguQcvwLgXDT8Vkl2qj/UEQ0J9gD8beHjTFk1w==} + resolution: + { + integrity: sha512-99jPl8JkCSCa4VlboNU1XuL98ijm74Pm9CGo6H4BoMVoVh1uhguQcvwLgXDT8Vkl2qj/UEQ0J9gD8beHjTFk1w==, + } hasBin: true fast-string-truncated-width@3.0.3: - resolution: {integrity: sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==} + resolution: + { + integrity: sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==, + } fast-string-width@3.0.2: - resolution: {integrity: sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==} + resolution: + { + integrity: sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==, + } fast-wrap-ansi@0.2.2: - resolution: {integrity: sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==} + resolution: + { + integrity: sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==, + } fastq@1.19.1: - resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} + resolution: + { + integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==, + } fdir@6.5.0: - resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} - engines: {node: '>=12.0.0'} + resolution: + { + integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==, + } + engines: { node: ">=12.0.0" } peerDependencies: picomatch: ^3 || ^4 peerDependenciesMeta: @@ -2029,707 +3260,1256 @@ packages: optional: true file-uri-to-path@1.0.0: - resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} + resolution: + { + integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==, + } fill-range@7.1.1: - resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==, + } + engines: { node: ">=8" } fnv1a-64@0.1.2: - resolution: {integrity: sha512-mJbcILTPybAR1jdOwt/lVv8N2cffeJFFP+gVbSAwLEx1ujXH27RpPd8Rokec/KX9c76UYIB6Co+H4lQzxPnJfA==} + resolution: + { + integrity: sha512-mJbcILTPybAR1jdOwt/lVv8N2cffeJFFP+gVbSAwLEx1ujXH27RpPd8Rokec/KX9c76UYIB6Co+H4lQzxPnJfA==, + } foreground-child@3.3.1: - resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} - engines: {node: '>=14'} + resolution: + { + integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==, + } + engines: { node: ">=14" } fraction.js@5.3.4: - resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==} + resolution: + { + integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==, + } fresh@0.5.2: - resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} - engines: {node: '>= 0.6'} + resolution: + { + integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==, + } + engines: { node: ">= 0.6" } fresh@2.0.0: - resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} - engines: {node: '>= 0.8'} + resolution: + { + integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==, + } + engines: { node: ">= 0.8" } fs-extra@9.1.0: - resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==} - engines: {node: '>=10'} + resolution: + { + integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==, + } + engines: { node: ">=10" } fs.realpath@1.0.0: - resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + resolution: + { + integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==, + } fsevents@2.3.3: - resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} - engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + resolution: + { + integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==, + } + engines: { node: ^8.16.0 || ^10.6.0 || >=11.0.0 } os: [darwin] function-bind@1.1.2: - resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + resolution: + { + integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==, + } fuse.js@7.5.0: - resolution: {integrity: sha512-sQtrEfA+ez/3G0cCZecF70oqpCRttCexYUG4mUrtWL49ULUzUyxokt5kyqwtKzj1270RaKih+hcP3qLcumccow==} - engines: {node: '>=10'} + resolution: + { + integrity: sha512-sQtrEfA+ez/3G0cCZecF70oqpCRttCexYUG4mUrtWL49ULUzUyxokt5kyqwtKzj1270RaKih+hcP3qLcumccow==, + } + engines: { node: ">=10" } fzf@0.5.2: - resolution: {integrity: sha512-Tt4kuxLXFKHy8KT40zwsUPUkg1CrsgY25FxA2U/j/0WgEDCk3ddc/zLTCCcbSHX9FcKtLuVaDGtGE/STWC+j3Q==} + resolution: + { + integrity: sha512-Tt4kuxLXFKHy8KT40zwsUPUkg1CrsgY25FxA2U/j/0WgEDCk3ddc/zLTCCcbSHX9FcKtLuVaDGtGE/STWC+j3Q==, + } generator-function@2.0.1: - resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==, + } + engines: { node: ">= 0.4" } generic-names@4.0.0: - resolution: {integrity: sha512-ySFolZQfw9FoDb3ed9d80Cm9f0+r7qj+HJkWjeD9RBfpxEVTlVhol+gvaQB/78WbwYfbnNh8nWHHBSlg072y6A==} + resolution: + { + integrity: sha512-ySFolZQfw9FoDb3ed9d80Cm9f0+r7qj+HJkWjeD9RBfpxEVTlVhol+gvaQB/78WbwYfbnNh8nWHHBSlg072y6A==, + } gensync@1.0.0-beta.2: - resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} - engines: {node: '>=6.9.0'} + resolution: + { + integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==, + } + engines: { node: ">=6.9.0" } get-caller-file@2.0.5: - resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} - engines: {node: 6.* || 8.* || >= 10.*} + resolution: + { + integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==, + } + engines: { node: 6.* || 8.* || >= 10.* } get-east-asian-width@1.6.0: - resolution: {integrity: sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==, + } + engines: { node: ">=18" } get-intrinsic@1.3.0: - resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==, + } + engines: { node: ">= 0.4" } get-port-please@3.2.0: - resolution: {integrity: sha512-I9QVvBw5U/hw3RmWpYKRumUeaDgxTPd401x364rLmWBJcOQ753eov1eTgzDqRG9bqFIfDc7gfzcQEWrUri3o1A==} + resolution: + { + integrity: sha512-I9QVvBw5U/hw3RmWpYKRumUeaDgxTPd401x364rLmWBJcOQ753eov1eTgzDqRG9bqFIfDc7gfzcQEWrUri3o1A==, + } get-proto@1.0.1: - resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==, + } + engines: { node: ">= 0.4" } get-stream@8.0.1: - resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==} - engines: {node: '>=16'} + resolution: + { + integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==, + } + engines: { node: ">=16" } giget@3.2.0: - resolution: {integrity: sha512-GvHTWcykIR/fP8cj8dMpuMMkvaeJfPvYnhq0oW+chSeIr+ldX21ifU2Ms6KBoyKZQZmVaUAAhQ2EZ68KJF8a7A==} + resolution: + { + integrity: sha512-GvHTWcykIR/fP8cj8dMpuMMkvaeJfPvYnhq0oW+chSeIr+ldX21ifU2Ms6KBoyKZQZmVaUAAhQ2EZ68KJF8a7A==, + } hasBin: true giget@3.3.1: - resolution: {integrity: sha512-r+mvuDjrjMpsdw46Kmeydb8bdHm7wOKw8wNBtTndkjbPjgAp5oUJUxRE76wZFknxIPokfWvep2qSXK37aXE6zg==} + resolution: + { + integrity: sha512-r+mvuDjrjMpsdw46Kmeydb8bdHm7wOKw8wNBtTndkjbPjgAp5oUJUxRE76wZFknxIPokfWvep2qSXK37aXE6zg==, + } hasBin: true glob-parent@5.1.2: - resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} - engines: {node: '>= 6'} + resolution: + { + integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==, + } + engines: { node: ">= 6" } glob-parent@6.0.2: - resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} - engines: {node: '>=10.13.0'} + resolution: + { + integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==, + } + engines: { node: ">=10.13.0" } glob@10.5.0: - resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==} + resolution: + { + integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==, + } deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me hasBin: true glob@13.0.6: - resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==} - engines: {node: 18 || 20 || >=22} + resolution: + { + integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==, + } + engines: { node: 18 || 20 || >=22 } glob@7.2.3: - resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + resolution: + { + integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==, + } deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me global-directory@4.0.1: - resolution: {integrity: sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==, + } + engines: { node: ">=18" } globby@16.2.3: - resolution: {integrity: sha512-VZX7TV7jmd/pn71vdnLKtgwy1IWqc3KjI9x1/UtPkwoKk5fKrNLY30ltDe3cAM5xruIN7YuuaulFt133jRrKZg==} - engines: {node: '>=20'} + resolution: + { + integrity: sha512-VZX7TV7jmd/pn71vdnLKtgwy1IWqc3KjI9x1/UtPkwoKk5fKrNLY30ltDe3cAM5xruIN7YuuaulFt133jRrKZg==, + } + engines: { node: ">=20" } gopd@1.2.0: - resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==, + } + engines: { node: ">= 0.4" } graceful-fs@4.2.11: - resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + resolution: + { + integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==, + } gzip-size@7.0.0: - resolution: {integrity: sha512-O1Ld7Dr+nqPnmGpdhzLmMTQ4vAsD+rHwMm1NLUmoUFFymBOMKxCCrtDxqdBRYXdeEPEi3SyoR4TizJLQrnKBNA==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + resolution: + { + integrity: sha512-O1Ld7Dr+nqPnmGpdhzLmMTQ4vAsD+rHwMm1NLUmoUFFymBOMKxCCrtDxqdBRYXdeEPEi3SyoR4TizJLQrnKBNA==, + } + engines: { node: ^12.20.0 || ^14.13.1 || >=16.0.0 } h3@1.15.11: - resolution: {integrity: sha512-L3THSe2MPeBwgIZVSH5zLdBBU90TOxarvhK9d04IDY2AmVS8j2Jz2LIWtwsGOU3lu2I5jCN7FNvVfY2+XyF+mg==} + resolution: + { + integrity: sha512-L3THSe2MPeBwgIZVSH5zLdBBU90TOxarvhK9d04IDY2AmVS8j2Jz2LIWtwsGOU3lu2I5jCN7FNvVfY2+XyF+mg==, + } has-flag@4.0.0: - resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==, + } + engines: { node: ">=8" } has-symbols@1.1.0: - resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==, + } + engines: { node: ">= 0.4" } has-tostringtag@1.0.2: - resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==, + } + engines: { node: ">= 0.4" } hasown@2.0.3: - resolution: {integrity: sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==, + } + engines: { node: ">= 0.4" } he@1.2.0: - resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} + resolution: + { + integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==, + } hasBin: true hookable@5.5.3: - resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==} + resolution: + { + integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==, + } hookable@6.1.1: - resolution: {integrity: sha512-U9LYDy1CwhMCnprUfeAZWZGByVbhd54hwepegYTK7Pi5NvqEj63ifz5z+xukznehT7i6NIZRu89Ay1AZmRsLEQ==} + resolution: + { + integrity: sha512-U9LYDy1CwhMCnprUfeAZWZGByVbhd54hwepegYTK7Pi5NvqEj63ifz5z+xukznehT7i6NIZRu89Ay1AZmRsLEQ==, + } http-assert@1.5.0: - resolution: {integrity: sha512-uPpH7OKX4H25hBmU6G1jWNaqJGpTXxey+YOUizJUAgu0AjLUeC8D73hTrhvDS5D+GJN1DN1+hhc/eF/wpxtp0w==} - engines: {node: '>= 0.8'} + resolution: + { + integrity: sha512-uPpH7OKX4H25hBmU6G1jWNaqJGpTXxey+YOUizJUAgu0AjLUeC8D73hTrhvDS5D+GJN1DN1+hhc/eF/wpxtp0w==, + } + engines: { node: ">= 0.8" } http-errors@1.6.3: - resolution: {integrity: sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==} - engines: {node: '>= 0.6'} + resolution: + { + integrity: sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==, + } + engines: { node: ">= 0.6" } http-errors@1.8.1: - resolution: {integrity: sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==} - engines: {node: '>= 0.6'} + resolution: + { + integrity: sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==, + } + engines: { node: ">= 0.6" } http-errors@2.0.1: - resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} - engines: {node: '>= 0.8'} + resolution: + { + integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==, + } + engines: { node: ">= 0.8" } http-shutdown@1.2.2: - resolution: {integrity: sha512-S9wWkJ/VSY9/k4qcjG318bqJNruzE4HySUhFYknwmu6LBP97KLLfwNf+n4V1BHurvFNkSKLFnK/RsuUnRTf9Vw==} - engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} + resolution: + { + integrity: sha512-S9wWkJ/VSY9/k4qcjG318bqJNruzE4HySUhFYknwmu6LBP97KLLfwNf+n4V1BHurvFNkSKLFnK/RsuUnRTf9Vw==, + } + engines: { iojs: ">= 1.0.0", node: ">= 0.12.0" } https-proxy-agent@7.0.6: - resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} - engines: {node: '>= 14'} + resolution: + { + integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==, + } + engines: { node: ">= 14" } httpxy@0.5.5: - resolution: {integrity: sha512-uDjmnPyp1q4Sgzf3w+J/Fc6UqcCEj0x4Wjp7OqK5dGhNeDgpyrAmnS6ey8QWrX3SWDon2DMKf9sBa5X9+CVyMA==} + resolution: + { + integrity: sha512-uDjmnPyp1q4Sgzf3w+J/Fc6UqcCEj0x4Wjp7OqK5dGhNeDgpyrAmnS6ey8QWrX3SWDon2DMKf9sBa5X9+CVyMA==, + } human-signals@5.0.0: - resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==} - engines: {node: '>=16.17.0'} + resolution: + { + integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==, + } + engines: { node: ">=16.17.0" } ieee754@1.2.1: - resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + resolution: + { + integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==, + } ignore@7.0.6: - resolution: {integrity: sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==} - engines: {node: '>= 4'} + resolution: + { + integrity: sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==, + } + engines: { node: ">= 4" } image-meta@0.2.2: - resolution: {integrity: sha512-3MOLanc3sb3LNGWQl1RlQlNWURE5g32aUphrDyFeCsxBTk08iE3VNe4CwsUZ0Qs1X+EfX0+r29Sxdpza4B+yRA==} + resolution: + { + integrity: sha512-3MOLanc3sb3LNGWQl1RlQlNWURE5g32aUphrDyFeCsxBTk08iE3VNe4CwsUZ0Qs1X+EfX0+r29Sxdpza4B+yRA==, + } immutable@5.1.4: - resolution: {integrity: sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA==} + resolution: + { + integrity: sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA==, + } impound@1.1.6: - resolution: {integrity: sha512-ugavDQkE74SGrBjagNIwNVHNBxX14mmfQnTm4jFGzOoWwYsgihqV698BNr5xwRY9quKK4rEg8bc6ECltllMr+w==} + resolution: + { + integrity: sha512-ugavDQkE74SGrBjagNIwNVHNBxX14mmfQnTm4jFGzOoWwYsgihqV698BNr5xwRY9quKK4rEg8bc6ECltllMr+w==, + } inflight@1.0.6: - resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + resolution: + { + integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==, + } deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. inherits@2.0.3: - resolution: {integrity: sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==} + resolution: + { + integrity: sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==, + } inherits@2.0.4: - resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + resolution: + { + integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==, + } ini@4.1.1: - resolution: {integrity: sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + resolution: + { + integrity: sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==, + } + engines: { node: ^14.17.0 || ^16.13.0 || >=18.0.0 } ioredis@5.11.1: - resolution: {integrity: sha512-ehuGcf94bQXhfagULNXrJdfnWO38v070jxSx/qE87Kjzmu2fU7ro5EFAb+OPituLqgfyuQaym5DlrNydW2sJ9A==} - engines: {node: '>=12.22.0'} + resolution: + { + integrity: sha512-ehuGcf94bQXhfagULNXrJdfnWO38v070jxSx/qE87Kjzmu2fU7ro5EFAb+OPituLqgfyuQaym5DlrNydW2sJ9A==, + } + engines: { node: ">=12.22.0" } iron-webcrypto@1.2.1: - resolution: {integrity: sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg==} + resolution: + { + integrity: sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg==, + } is-binary-path@2.1.0: - resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==, + } + engines: { node: ">=8" } is-core-module@2.16.2: - resolution: {integrity: sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==, + } + engines: { node: ">= 0.4" } is-docker@2.2.1: - resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==, + } + engines: { node: ">=8" } hasBin: true is-docker@3.0.0: - resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + resolution: + { + integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==, + } + engines: { node: ^12.20.0 || ^14.13.1 || >=16.0.0 } hasBin: true is-extglob@2.1.1: - resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} - engines: {node: '>=0.10.0'} + resolution: + { + integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==, + } + engines: { node: ">=0.10.0" } is-fullwidth-code-point@3.0.0: - resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==, + } + engines: { node: ">=8" } is-generator-function@1.1.2: - resolution: {integrity: sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==, + } + engines: { node: ">= 0.4" } is-glob@4.0.3: - resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} - engines: {node: '>=0.10.0'} + resolution: + { + integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==, + } + engines: { node: ">=0.10.0" } is-in-ssh@1.0.0: - resolution: {integrity: sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw==} - engines: {node: '>=20'} + resolution: + { + integrity: sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw==, + } + engines: { node: ">=20" } is-inside-container@1.0.0: - resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==} - engines: {node: '>=14.16'} + resolution: + { + integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==, + } + engines: { node: ">=14.16" } hasBin: true is-installed-globally@1.0.0: - resolution: {integrity: sha512-K55T22lfpQ63N4KEN57jZUAaAYqYHEe8veb/TycJRk9DdSCLLcovXz/mL6mOnhQaZsQGwPhuFopdQIlqGSEjiQ==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-K55T22lfpQ63N4KEN57jZUAaAYqYHEe8veb/TycJRk9DdSCLLcovXz/mL6mOnhQaZsQGwPhuFopdQIlqGSEjiQ==, + } + engines: { node: ">=18" } is-module@1.0.0: - resolution: {integrity: sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==} + resolution: + { + integrity: sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==, + } is-number@7.0.0: - resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} - engines: {node: '>=0.12.0'} + resolution: + { + integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==, + } + engines: { node: ">=0.12.0" } is-path-inside@4.0.0: - resolution: {integrity: sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA==, + } + engines: { node: ">=12" } is-reference@1.2.1: - resolution: {integrity: sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==} + resolution: + { + integrity: sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==, + } is-regex@1.2.1: - resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==, + } + engines: { node: ">= 0.4" } is-stream@2.0.1: - resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==, + } + engines: { node: ">=8" } is-stream@3.0.0: - resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + resolution: + { + integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==, + } + engines: { node: ^12.20.0 || ^14.13.1 || >=16.0.0 } is-wsl@2.2.0: - resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==, + } + engines: { node: ">=8" } is-wsl@3.1.1: - resolution: {integrity: sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==} - engines: {node: '>=16'} + resolution: + { + integrity: sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==, + } + engines: { node: ">=16" } isarray@1.0.0: - resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + resolution: + { + integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==, + } isexe@2.0.0: - resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + resolution: + { + integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==, + } isexe@4.0.0: - resolution: {integrity: sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==} - engines: {node: '>=20'} + resolution: + { + integrity: sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==, + } + engines: { node: ">=20" } jackspeak@3.4.3: - resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + resolution: + { + integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==, + } jiti@1.21.7: - resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==} + resolution: + { + integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==, + } hasBin: true jiti@2.7.0: - resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} + resolution: + { + integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==, + } hasBin: true js-tokens@10.0.0: - resolution: {integrity: sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==} + resolution: + { + integrity: sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==, + } js-tokens@4.0.0: - resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + resolution: + { + integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==, + } jsesc@3.1.0: - resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} - engines: {node: '>=6'} + resolution: + { + integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==, + } + engines: { node: ">=6" } hasBin: true json5@2.2.3: - resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} - engines: {node: '>=6'} + resolution: + { + integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==, + } + engines: { node: ">=6" } hasBin: true jsonfile@6.2.0: - resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==} + resolution: + { + integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==, + } keygrip@1.1.0: - resolution: {integrity: sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ==} - engines: {node: '>= 0.6'} + resolution: + { + integrity: sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ==, + } + engines: { node: ">= 0.6" } kleur@4.1.5: - resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} - engines: {node: '>=6'} + resolution: + { + integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==, + } + engines: { node: ">=6" } klona@2.0.6: - resolution: {integrity: sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==} - engines: {node: '>= 8'} + resolution: + { + integrity: sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==, + } + engines: { node: ">= 8" } knitwork@1.3.0: - resolution: {integrity: sha512-4LqMNoONzR43B1W0ek0fhXMsDNW/zxa1NdFAVMY+k28pgZLovR4G3PB5MrpTxCy1QaZCqNoiaKPr5w5qZHfSNw==} + resolution: + { + integrity: sha512-4LqMNoONzR43B1W0ek0fhXMsDNW/zxa1NdFAVMY+k28pgZLovR4G3PB5MrpTxCy1QaZCqNoiaKPr5w5qZHfSNw==, + } koa-compose@4.1.0: - resolution: {integrity: sha512-8ODW8TrDuMYvXRwra/Kh7/rJo9BtOfPc6qO8eAfC80CnCvSjSl0bkRM24X6/XBBEyj0v1nRUQ1LyOy3dbqOWXw==} + resolution: + { + integrity: sha512-8ODW8TrDuMYvXRwra/Kh7/rJo9BtOfPc6qO8eAfC80CnCvSjSl0bkRM24X6/XBBEyj0v1nRUQ1LyOy3dbqOWXw==, + } koa-convert@2.0.0: - resolution: {integrity: sha512-asOvN6bFlSnxewce2e/DK3p4tltyfC4VM7ZwuTuepI7dEQVcvpyFuBcEARu1+Hxg8DIwytce2n7jrZtRlPrARA==} - engines: {node: '>= 10'} + resolution: + { + integrity: sha512-asOvN6bFlSnxewce2e/DK3p4tltyfC4VM7ZwuTuepI7dEQVcvpyFuBcEARu1+Hxg8DIwytce2n7jrZtRlPrARA==, + } + engines: { node: ">= 10" } koa-send@5.0.1: - resolution: {integrity: sha512-tmcyQ/wXXuxpDxyNXv5yNNkdAMdFRqwtegBXUaowiQzUKqJehttS0x2j0eOZDQAyloAth5w6wwBImnFzkUz3pQ==} - engines: {node: '>= 8'} + resolution: + { + integrity: sha512-tmcyQ/wXXuxpDxyNXv5yNNkdAMdFRqwtegBXUaowiQzUKqJehttS0x2j0eOZDQAyloAth5w6wwBImnFzkUz3pQ==, + } + engines: { node: ">= 8" } koa-static@5.0.0: - resolution: {integrity: sha512-UqyYyH5YEXaJrf9S8E23GoJFQZXkBVJ9zYYMPGz919MSX1KuvAcycIuS0ci150HCoPf4XQVhQ84Qf8xRPWxFaQ==} - engines: {node: '>= 7.6.0'} + resolution: + { + integrity: sha512-UqyYyH5YEXaJrf9S8E23GoJFQZXkBVJ9zYYMPGz919MSX1KuvAcycIuS0ci150HCoPf4XQVhQ84Qf8xRPWxFaQ==, + } + engines: { node: ">= 7.6.0" } koa@2.16.3: - resolution: {integrity: sha512-zPPuIt+ku1iCpFBRwseMcPYQ1cJL8l60rSmKeOuGfOXyE6YnTBmf2aEFNL2HQGrD0cPcLO/t+v9RTgC+fwEh/g==} - engines: {node: ^4.8.4 || ^6.10.1 || ^7.10.1 || >= 8.1.4} + resolution: + { + integrity: sha512-zPPuIt+ku1iCpFBRwseMcPYQ1cJL8l60rSmKeOuGfOXyE6YnTBmf2aEFNL2HQGrD0cPcLO/t+v9RTgC+fwEh/g==, + } + engines: { node: ^4.8.4 || ^6.10.1 || ^7.10.1 || >= 8.1.4 } launch-editor@2.14.1: - resolution: {integrity: sha512-QWBrQsMpH7gPr965dsKD/3cKWiNoTjpATQf++Xq63N6sKRGMwlVXz41O1IZTMfZQgBctD/K5Zt06+/I6pP6+HA==} + resolution: + { + integrity: sha512-QWBrQsMpH7gPr965dsKD/3cKWiNoTjpATQf++Xq63N6sKRGMwlVXz41O1IZTMfZQgBctD/K5Zt06+/I6pP6+HA==, + } lazystream@1.0.1: - resolution: {integrity: sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==} - engines: {node: '>= 0.6.3'} + resolution: + { + integrity: sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==, + } + engines: { node: ">= 0.6.3" } lightningcss-android-arm64@1.33.0: - resolution: {integrity: sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==} - engines: {node: '>= 12.0.0'} + resolution: + { + integrity: sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==, + } + engines: { node: ">= 12.0.0" } cpu: [arm64] os: [android] lightningcss-darwin-arm64@1.33.0: - resolution: {integrity: sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==} - engines: {node: '>= 12.0.0'} + resolution: + { + integrity: sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==, + } + engines: { node: ">= 12.0.0" } cpu: [arm64] os: [darwin] lightningcss-darwin-x64@1.33.0: - resolution: {integrity: sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==} - engines: {node: '>= 12.0.0'} + resolution: + { + integrity: sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==, + } + engines: { node: ">= 12.0.0" } cpu: [x64] os: [darwin] lightningcss-freebsd-x64@1.33.0: - resolution: {integrity: sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==} - engines: {node: '>= 12.0.0'} + resolution: + { + integrity: sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==, + } + engines: { node: ">= 12.0.0" } cpu: [x64] os: [freebsd] lightningcss-linux-arm-gnueabihf@1.33.0: - resolution: {integrity: sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==} - engines: {node: '>= 12.0.0'} + resolution: + { + integrity: sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==, + } + engines: { node: ">= 12.0.0" } cpu: [arm] os: [linux] lightningcss-linux-arm64-gnu@1.33.0: - resolution: {integrity: sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==} - engines: {node: '>= 12.0.0'} + resolution: + { + integrity: sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==, + } + engines: { node: ">= 12.0.0" } cpu: [arm64] os: [linux] libc: [glibc] lightningcss-linux-arm64-musl@1.33.0: - resolution: {integrity: sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==} - engines: {node: '>= 12.0.0'} + resolution: + { + integrity: sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==, + } + engines: { node: ">= 12.0.0" } cpu: [arm64] os: [linux] libc: [musl] lightningcss-linux-x64-gnu@1.33.0: - resolution: {integrity: sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==} - engines: {node: '>= 12.0.0'} + resolution: + { + integrity: sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==, + } + engines: { node: ">= 12.0.0" } cpu: [x64] os: [linux] libc: [glibc] lightningcss-linux-x64-musl@1.33.0: - resolution: {integrity: sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==} - engines: {node: '>= 12.0.0'} + resolution: + { + integrity: sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==, + } + engines: { node: ">= 12.0.0" } cpu: [x64] os: [linux] libc: [musl] lightningcss-win32-arm64-msvc@1.33.0: - resolution: {integrity: sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==} - engines: {node: '>= 12.0.0'} + resolution: + { + integrity: sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==, + } + engines: { node: ">= 12.0.0" } cpu: [arm64] os: [win32] lightningcss-win32-x64-msvc@1.33.0: - resolution: {integrity: sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==} - engines: {node: '>= 12.0.0'} + resolution: + { + integrity: sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==, + } + engines: { node: ">= 12.0.0" } cpu: [x64] os: [win32] lightningcss@1.33.0: - resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==} - engines: {node: '>= 12.0.0'} + resolution: + { + integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==, + } + engines: { node: ">= 12.0.0" } lilconfig@3.1.3: - resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} - engines: {node: '>=14'} + resolution: + { + integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==, + } + engines: { node: ">=14" } lines-and-columns@1.2.4: - resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + resolution: + { + integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==, + } linkify-it@5.0.1: - resolution: {integrity: sha512-wVoTjP4Q6R0NW5hiZkVJaFZPWgtXfoGF+6LucL3/FtiNjmcHhYjEr5f1Kqjirc1nBW07J/ZuRFumqr2oqccEWg==} + resolution: + { + integrity: sha512-wVoTjP4Q6R0NW5hiZkVJaFZPWgtXfoGF+6LucL3/FtiNjmcHhYjEr5f1Kqjirc1nBW07J/ZuRFumqr2oqccEWg==, + } listhen@1.10.1: - resolution: {integrity: sha512-6nt/86SkqUQSLW1ofz8MxC6RhRMqOl3ONISe6qqvJ3xj09aJWQx6DhgSZpugs3PX4PXdOas/WD6A9jx6J2N19A==} + resolution: + { + integrity: sha512-6nt/86SkqUQSLW1ofz8MxC6RhRMqOl3ONISe6qqvJ3xj09aJWQx6DhgSZpugs3PX4PXdOas/WD6A9jx6J2N19A==, + } hasBin: true peerDependencies: - '@parcel/watcher': ^2.5.6 + "@parcel/watcher": ^2.5.6 peerDependenciesMeta: - '@parcel/watcher': + "@parcel/watcher": optional: true loader-utils@3.3.1: - resolution: {integrity: sha512-FMJTLMXfCLMLfJxcX9PFqX5qD88Z5MRGaZCVzfuqeZSPsyiBzs+pahDQjbIWz2QIzPZz0NX9Zy4FX3lmK6YHIg==} - engines: {node: '>= 12.13.0'} + resolution: + { + integrity: sha512-FMJTLMXfCLMLfJxcX9PFqX5qD88Z5MRGaZCVzfuqeZSPsyiBzs+pahDQjbIWz2QIzPZz0NX9Zy4FX3lmK6YHIg==, + } + engines: { node: ">= 12.13.0" } local-pkg@1.2.1: - resolution: {integrity: sha512-++gUqRDEvcnN6Zhqrr+y/CkVEHhlrR96vZn3nZZPYzMcBUyBtTKzB9NadClFIsIVSsu+3i9tfk/erqy9kAmt7Q==} - engines: {node: '>=14'} + resolution: + { + integrity: sha512-++gUqRDEvcnN6Zhqrr+y/CkVEHhlrR96vZn3nZZPYzMcBUyBtTKzB9NadClFIsIVSsu+3i9tfk/erqy9kAmt7Q==, + } + engines: { node: ">=14" } lodash@4.18.1: - resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==} + resolution: + { + integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==, + } lru-cache@10.4.3: - resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + resolution: + { + integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==, + } lru-cache@11.5.2: - resolution: {integrity: sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==} - engines: {node: 20 || >=22} + resolution: + { + integrity: sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==, + } + engines: { node: 20 || >=22 } lru-cache@5.1.1: - resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + resolution: + { + integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==, + } magic-string-ast@1.0.3: - resolution: {integrity: sha512-CvkkH1i81zl7mmb94DsRiFeG9V2fR2JeuK8yDgS8oiZSFa++wWLEgZ5ufEOyLHbvSbD1gTRKv9NdX69Rnvr9JA==} - engines: {node: '>=20.19.0'} + resolution: + { + integrity: sha512-CvkkH1i81zl7mmb94DsRiFeG9V2fR2JeuK8yDgS8oiZSFa++wWLEgZ5ufEOyLHbvSbD1gTRKv9NdX69Rnvr9JA==, + } + engines: { node: ">=20.19.0" } magic-string@0.30.21: - resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + resolution: + { + integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==, + } magic-string@1.1.0: - resolution: {integrity: sha512-kS3VHe0nEPST2saQV4Rbkchcd3UBRkVTQHo1D3h/ZTwFDhai/mfKkmtPAtD129EOI7K3HlHIsFOt0WrI2/oU9g==} + resolution: + { + integrity: sha512-kS3VHe0nEPST2saQV4Rbkchcd3UBRkVTQHo1D3h/ZTwFDhai/mfKkmtPAtD129EOI7K3HlHIsFOt0WrI2/oU9g==, + } magicast@0.5.4: - resolution: {integrity: sha512-llBEhWm1SacoRwgHUoQJYtwp4PBLF4faQi5TCpIGyGs9n4y5+juI0tDgyKIfpqxckRHaHzouUEph3THklWh03w==} + resolution: + { + integrity: sha512-llBEhWm1SacoRwgHUoQJYtwp4PBLF4faQi5TCpIGyGs9n4y5+juI0tDgyKIfpqxckRHaHzouUEph3THklWh03w==, + } markdown-it@14.2.0: - resolution: {integrity: sha512-1TGiQiJVRQ3NPmZH6sx5Cfnmg6GQm9jvC1ch4TK511NjSJvjzKLzn5pPfZRNZkRPZP0HqCioSndqH8v2nRaWVQ==} + resolution: + { + integrity: sha512-1TGiQiJVRQ3NPmZH6sx5Cfnmg6GQm9jvC1ch4TK511NjSJvjzKLzn5pPfZRNZkRPZP0HqCioSndqH8v2nRaWVQ==, + } hasBin: true math-intrinsics@1.1.0: - resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==, + } + engines: { node: ">= 0.4" } mdn-data@2.0.28: - resolution: {integrity: sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==} + resolution: + { + integrity: sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==, + } mdn-data@2.27.1: - resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==} + resolution: + { + integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==, + } mdurl@2.0.0: - resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==} + resolution: + { + integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==, + } media-typer@0.3.0: - resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} - engines: {node: '>= 0.6'} + resolution: + { + integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==, + } + engines: { node: ">= 0.6" } merge-stream@2.0.0: - resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + resolution: + { + integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==, + } merge2@1.4.1: - resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} - engines: {node: '>= 8'} + resolution: + { + integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==, + } + engines: { node: ">= 8" } methods@1.1.2: - resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} - engines: {node: '>= 0.6'} + resolution: + { + integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==, + } + engines: { node: ">= 0.6" } micromark-core-commonmark@2.0.3: - resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} + resolution: + { + integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==, + } micromark-factory-destination@2.0.1: - resolution: {integrity: sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==} + resolution: + { + integrity: sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==, + } micromark-factory-label@2.0.1: - resolution: {integrity: sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==} + resolution: + { + integrity: sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==, + } micromark-factory-space@2.0.1: - resolution: {integrity: sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==} + resolution: + { + integrity: sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==, + } micromark-factory-title@2.0.1: - resolution: {integrity: sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==} + resolution: + { + integrity: sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==, + } micromark-factory-whitespace@2.0.1: - resolution: {integrity: sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==} + resolution: + { + integrity: sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==, + } micromark-util-character@2.1.1: - resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==} + resolution: + { + integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==, + } micromark-util-chunked@2.0.1: - resolution: {integrity: sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==} + resolution: + { + integrity: sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==, + } micromark-util-classify-character@2.0.1: - resolution: {integrity: sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==} + resolution: + { + integrity: sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==, + } micromark-util-combine-extensions@2.0.1: - resolution: {integrity: sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==} + resolution: + { + integrity: sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==, + } micromark-util-decode-numeric-character-reference@2.0.2: - resolution: {integrity: sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==} + resolution: + { + integrity: sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==, + } micromark-util-encode@2.0.1: - resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==} + resolution: + { + integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==, + } micromark-util-html-tag-name@2.0.1: - resolution: {integrity: sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==} + resolution: + { + integrity: sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==, + } micromark-util-normalize-identifier@2.0.1: - resolution: {integrity: sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==} + resolution: + { + integrity: sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==, + } micromark-util-resolve-all@2.0.1: - resolution: {integrity: sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==} + resolution: + { + integrity: sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==, + } micromark-util-sanitize-uri@2.0.1: - resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==} + resolution: + { + integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==, + } micromark-util-subtokenize@2.1.0: - resolution: {integrity: sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==} + resolution: + { + integrity: sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==, + } micromark-util-symbol@2.0.1: - resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==} + resolution: + { + integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==, + } micromark-util-types@2.0.2: - resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==} + resolution: + { + integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==, + } micromark@4.0.2: - resolution: {integrity: sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==} + resolution: + { + integrity: sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==, + } micromatch@4.0.8: - resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} - engines: {node: '>=8.6'} + resolution: + { + integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==, + } + engines: { node: ">=8.6" } mime-db@1.52.0: - resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} - engines: {node: '>= 0.6'} + resolution: + { + integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==, + } + engines: { node: ">= 0.6" } mime-db@1.54.0: - resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} - engines: {node: '>= 0.6'} + resolution: + { + integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==, + } + engines: { node: ">= 0.6" } mime-types@2.1.35: - resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} - engines: {node: '>= 0.6'} + resolution: + { + integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==, + } + engines: { node: ">= 0.6" } mime-types@3.0.2: - resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==, + } + engines: { node: ">=18" } mime@4.1.0: - resolution: {integrity: sha512-X5ju04+cAzsojXKes0B/S4tcYtFAJ6tTMuSPBEn9CPGlrWr8Fiw7qYeLT0XyH80HSoAoqWCaz+MWKh22P7G1cw==} - engines: {node: '>=16'} + resolution: + { + integrity: sha512-X5ju04+cAzsojXKes0B/S4tcYtFAJ6tTMuSPBEn9CPGlrWr8Fiw7qYeLT0XyH80HSoAoqWCaz+MWKh22P7G1cw==, + } + engines: { node: ">=16" } hasBin: true mimic-fn@4.0.0: - resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==, + } + engines: { node: ">=12" } mini-svg-data-uri@1.4.4: - resolution: {integrity: sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==} + resolution: + { + integrity: sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==, + } hasBin: true minimatch@10.2.6: - resolution: {integrity: sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==} - engines: {node: 18 || 20 || >=22} + resolution: + { + integrity: sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==, + } + engines: { node: 18 || 20 || >=22 } minimatch@3.1.2: - resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + resolution: + { + integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==, + } minimatch@5.1.9: - resolution: {integrity: sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==} - engines: {node: '>=10'} + resolution: + { + integrity: sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==, + } + engines: { node: ">=10" } minimatch@9.0.5: - resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} - engines: {node: '>=16 || 14 >=14.17'} + resolution: + { + integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==, + } + engines: { node: ">=16 || 14 >=14.17" } minipass@7.1.3: - resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} - engines: {node: '>=16 || 14 >=14.17'} + resolution: + { + integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==, + } + engines: { node: ">=16 || 14 >=14.17" } minizlib@3.1.0: - resolution: {integrity: sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==} - engines: {node: '>= 18'} + resolution: + { + integrity: sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==, + } + engines: { node: ">= 18" } mlly@1.8.2: - resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==} + resolution: + { + integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==, + } mocked-exports@0.1.1: - resolution: {integrity: sha512-aF7yRQr/Q0O2/4pIXm6PZ5G+jAd7QS4Yu8m+WEeEHGnbo+7mE36CbLSDQiXYV8bVL3NfmdeqPJct0tUlnjVSnA==} + resolution: + { + integrity: sha512-aF7yRQr/Q0O2/4pIXm6PZ5G+jAd7QS4Yu8m+WEeEHGnbo+7mE36CbLSDQiXYV8bVL3NfmdeqPJct0tUlnjVSnA==, + } mrmime@2.0.1: - resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} - engines: {node: '>=10'} + resolution: + { + integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==, + } + engines: { node: ">=10" } ms@2.1.3: - resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + resolution: + { + integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==, + } muggle-string@0.4.1: - resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==} + resolution: + { + integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==, + } mz@2.7.0: - resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + resolution: + { + integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==, + } nanoid@3.3.16: - resolution: {integrity: sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==} - engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + resolution: + { + integrity: sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==, + } + engines: { node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1 } hasBin: true nanoid@3.3.18: - resolution: {integrity: sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==} - engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + resolution: + { + integrity: sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==, + } + engines: { node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1 } hasBin: true nanotar@0.3.0: - resolution: {integrity: sha512-Kv2JYYiCzt16Kt5QwAc9BFG89xfPNBx+oQL4GQXD9nLqPkZBiNaqaCWtwnbk/q7UVsTYevvM1b0UF8zmEI4pCg==} + resolution: + { + integrity: sha512-Kv2JYYiCzt16Kt5QwAc9BFG89xfPNBx+oQL4GQXD9nLqPkZBiNaqaCWtwnbk/q7UVsTYevvM1b0UF8zmEI4pCg==, + } negotiator@0.6.3: - resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} - engines: {node: '>= 0.6'} + resolution: + { + integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==, + } + engines: { node: ">= 0.6" } nitropack@2.13.4: - resolution: {integrity: sha512-tX7bT6zxNeMwkc6hxHiZeUoTOjVrcjoh1Z3cmxOlodIqjl4HISgqfGOmkWSayky3Nv9Z5+KQH52F8nmXJY5AAA==} - engines: {node: ^20.19.0 || >=22.12.0} + resolution: + { + integrity: sha512-tX7bT6zxNeMwkc6hxHiZeUoTOjVrcjoh1Z3cmxOlodIqjl4HISgqfGOmkWSayky3Nv9Z5+KQH52F8nmXJY5AAA==, + } + engines: { node: ^20.19.0 || >=22.12.0 } hasBin: true peerDependencies: xml2js: ^0.6.2 @@ -2738,14 +4518,23 @@ packages: optional: true node-addon-api@7.1.1: - resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} + resolution: + { + integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==, + } node-fetch-native@1.6.7: - resolution: {integrity: sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==} + resolution: + { + integrity: sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==, + } node-fetch@2.7.0: - resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} - engines: {node: 4.x || >=6.0.0} + resolution: + { + integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==, + } + engines: { node: 4.x || >=6.0.0 } peerDependencies: encoding: ^0.1.0 peerDependenciesMeta: @@ -2753,129 +4542,219 @@ packages: optional: true node-forge@1.4.0: - resolution: {integrity: sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ==} - engines: {node: '>= 6.13.0'} + resolution: + { + integrity: sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ==, + } + engines: { node: ">= 6.13.0" } node-gyp-build@4.8.4: - resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==} + resolution: + { + integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==, + } hasBin: true node-mock-http@1.0.4: - resolution: {integrity: sha512-8DY+kFsDkNXy1sJglUfuODx1/opAGJGyrTuFqEoN90oRc2Vk0ZbD4K2qmKXBBEhZQzdKHIVfEJpDU8Ak2NJEvQ==} + resolution: + { + integrity: sha512-8DY+kFsDkNXy1sJglUfuODx1/opAGJGyrTuFqEoN90oRc2Vk0ZbD4K2qmKXBBEhZQzdKHIVfEJpDU8Ak2NJEvQ==, + } node-mock-http@1.0.5: - resolution: {integrity: sha512-KQyt/wLjG3TAc7DOUhpqWzgd4ERxR80JOlTK5VE5R1S12IaPVN5qkj4klBce9HPG1Njuup4Sb5bljaT34lIyjw==} + resolution: + { + integrity: sha512-KQyt/wLjG3TAc7DOUhpqWzgd4ERxR80JOlTK5VE5R1S12IaPVN5qkj4klBce9HPG1Njuup4Sb5bljaT34lIyjw==, + } node-releases@2.0.44: - resolution: {integrity: sha512-5WUyunoPMsvvEhS8AxHtRzP+oA8UCkJ7YRxatWKjngndhDGLiqEVAQKWjFAiAiuL8zMRGzGSJxFnLetoa43qGQ==} + resolution: + { + integrity: sha512-5WUyunoPMsvvEhS8AxHtRzP+oA8UCkJ7YRxatWKjngndhDGLiqEVAQKWjFAiAiuL8zMRGzGSJxFnLetoa43qGQ==, + } node-releases@2.0.53: - resolution: {integrity: sha512-D9UOmYG3UH1V+ENW56t5QXBwJw1YEY18ruVeus89Rw+SyIgjPkCO84bRzO3uNIYosJbNwiabWVn48o3uJLjxFQ==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-D9UOmYG3UH1V+ENW56t5QXBwJw1YEY18ruVeus89Rw+SyIgjPkCO84bRzO3uNIYosJbNwiabWVn48o3uJLjxFQ==, + } + engines: { node: ">=18" } nopt@8.1.0: - resolution: {integrity: sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A==} - engines: {node: ^18.17.0 || >=20.5.0} + resolution: + { + integrity: sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A==, + } + engines: { node: ^18.17.0 || >=20.5.0 } hasBin: true normalize-path@3.0.0: - resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} - engines: {node: '>=0.10.0'} + resolution: + { + integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==, + } + engines: { node: ">=0.10.0" } nostics@1.2.0: - resolution: {integrity: sha512-FGqEfhQjrvo1lL8KFifdTQiNwwQHJxC1jtYE1Rc54qF/jxONUNL+kC9gS1krX8Q65PgrQ5fCqH/I4NhWBvdSqg==} + resolution: + { + integrity: sha512-FGqEfhQjrvo1lL8KFifdTQiNwwQHJxC1jtYE1Rc54qF/jxONUNL+kC9gS1krX8Q65PgrQ5fCqH/I4NhWBvdSqg==, + } npm-run-path@5.3.0: - resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + resolution: + { + integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==, + } + engines: { node: ^12.20.0 || ^14.13.1 || >=16.0.0 } npm-run-path@6.0.0: - resolution: {integrity: sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==, + } + engines: { node: ">=18" } nth-check@2.1.1: - resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + resolution: + { + integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==, + } nuxt@4.5.1: - resolution: {integrity: sha512-bDfkB3VKenF7diLS+r6hBGjW/5hyH35q2xHZ355jEYuD1/dVC/3DW8yW8P+8p+Qk8MX+J0TmD66+jxnCandEpA==} - engines: {node: ^22.19.0 || ^24.11.0 || >=26.0.0} + resolution: + { + integrity: sha512-bDfkB3VKenF7diLS+r6hBGjW/5hyH35q2xHZ355jEYuD1/dVC/3DW8yW8P+8p+Qk8MX+J0TmD66+jxnCandEpA==, + } + engines: { node: ^22.19.0 || ^24.11.0 || >=26.0.0 } hasBin: true peerDependencies: - '@parcel/watcher': ^2.1.0 - '@types/node': '>=18.12.0' + "@parcel/watcher": ^2.1.0 + "@types/node": ">=18.12.0" peerDependenciesMeta: - '@parcel/watcher': + "@parcel/watcher": optional: true - '@types/node': + "@types/node": optional: true nypm@0.6.9: - resolution: {integrity: sha512-zxlE2yvSWZWmHcNdT3+5zV2lrCogeE9YOklHrR3dFjqutq5wO7GFDYLFDRXLsYnJzwvy/im9fYoxePvS0VTW0w==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-zxlE2yvSWZWmHcNdT3+5zV2lrCogeE9YOklHrR3dFjqutq5wO7GFDYLFDRXLsYnJzwvy/im9fYoxePvS0VTW0w==, + } + engines: { node: ">=18" } hasBin: true object-assign@4.1.1: - resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} - engines: {node: '>=0.10.0'} + resolution: + { + integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==, + } + engines: { node: ">=0.10.0" } object-hash@3.0.0: - resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} - engines: {node: '>= 6'} + resolution: + { + integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==, + } + engines: { node: ">= 6" } object-identity@0.2.3: - resolution: {integrity: sha512-2J8Joz2Tf7aaylhqFvIUJHNgpuGR38Hh75Voq9GzTbStBxJUaOtN0K1aOd3cV5qp+ij1pMqRbPrGCGMOyX303w==} + resolution: + { + integrity: sha512-2J8Joz2Tf7aaylhqFvIUJHNgpuGR38Hh75Voq9GzTbStBxJUaOtN0K1aOd3cV5qp+ij1pMqRbPrGCGMOyX303w==, + } obug@2.1.4: - resolution: {integrity: sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==} - engines: {node: '>=12.20.0'} + resolution: + { + integrity: sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==, + } + engines: { node: ">=12.20.0" } ofetch@1.5.1: - resolution: {integrity: sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA==} + resolution: + { + integrity: sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA==, + } ofetch@2.0.0-alpha.3: - resolution: {integrity: sha512-zpYTCs2byOuft65vI3z43Dd6iSdFbOZZLb9/d21aCpx2rGastVU9dOCv0lu4ykc1Ur1anAYjDi3SUvR0vq50JA==} + resolution: + { + integrity: sha512-zpYTCs2byOuft65vI3z43Dd6iSdFbOZZLb9/d21aCpx2rGastVU9dOCv0lu4ykc1Ur1anAYjDi3SUvR0vq50JA==, + } ohash@2.0.11: - resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==} + resolution: + { + integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==, + } ometa@0.2.2: - resolution: {integrity: sha512-LZuoK/yjU3FvrxPjUXUlZ1bavCfBPqauA7fsNdwi+AVhRdyk2IzgP3JRnevvjzQ6fKHdUw8YISshf53FmpHrng==} - engines: {node: '>= 0.2.0'} + resolution: + { + integrity: sha512-LZuoK/yjU3FvrxPjUXUlZ1bavCfBPqauA7fsNdwi+AVhRdyk2IzgP3JRnevvjzQ6fKHdUw8YISshf53FmpHrng==, + } + engines: { node: ">= 0.2.0" } on-change@6.0.2: - resolution: {integrity: sha512-08+12qcOVEA0fS9g/VxKS27HaT94nRutUT77J2dr8zv/unzXopvhBuF8tNLWsoLQ5IgrQ6eptGeGqUYat82U1w==} - engines: {node: '>=20'} + resolution: + { + integrity: sha512-08+12qcOVEA0fS9g/VxKS27HaT94nRutUT77J2dr8zv/unzXopvhBuF8tNLWsoLQ5IgrQ6eptGeGqUYat82U1w==, + } + engines: { node: ">=20" } on-finished@2.4.1: - resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} - engines: {node: '>= 0.8'} + resolution: + { + integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==, + } + engines: { node: ">= 0.8" } once@1.4.0: - resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + resolution: + { + integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==, + } onetime@6.0.0: - resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==, + } + engines: { node: ">=12" } only@0.0.2: - resolution: {integrity: sha512-Fvw+Jemq5fjjyWz6CpKx6w9s7xxqo3+JCyM0WXWeCSOboZ8ABkyvP8ID4CZuChA/wxSx+XSJmdOm8rGVyJ1hdQ==} + resolution: + { + integrity: sha512-Fvw+Jemq5fjjyWz6CpKx6w9s7xxqo3+JCyM0WXWeCSOboZ8ABkyvP8ID4CZuChA/wxSx+XSJmdOm8rGVyJ1hdQ==, + } open@11.0.0: - resolution: {integrity: sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw==} - engines: {node: '>=20'} + resolution: + { + integrity: sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw==, + } + engines: { node: ">=20" } open@7.4.2: - resolution: {integrity: sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==, + } + engines: { node: ">=8" } oxc-walker@1.1.1: - resolution: {integrity: sha512-Hwd7dq28zu/Er99+bpWp16CGwFrhyalJh0W3JOB7XpPvgWo3MqMi2ksWGKuzzA9zt50mJ2pIUwR5lpAdZ9ACNQ==} + resolution: + { + integrity: sha512-Hwd7dq28zu/Er99+bpWp16CGwFrhyalJh0W3JOB7XpPvgWo3MqMi2ksWGKuzzA9zt50mJ2pIUwR5lpAdZ9ACNQ==, + } peerDependencies: - '@oxc-project/types': '>=0.98.0' - oxc-parser: '>=0.98.0' - rolldown: '>=1.0.0' + "@oxc-project/types": ">=0.98.0" + oxc-parser: ">=0.98.0" + rolldown: ">=1.0.0" peerDependenciesMeta: - '@oxc-project/types': + "@oxc-project/types": optional: true oxc-parser: optional: true @@ -2883,136 +4762,226 @@ packages: optional: true package-json-from-dist@1.0.1: - resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + resolution: + { + integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==, + } parseurl@1.3.3: - resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} - engines: {node: '>= 0.8'} + resolution: + { + integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==, + } + engines: { node: ">= 0.8" } path-browserify@1.0.1: - resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} + resolution: + { + integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==, + } path-is-absolute@1.0.1: - resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} - engines: {node: '>=0.10.0'} + resolution: + { + integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==, + } + engines: { node: ">=0.10.0" } path-key@3.1.1: - resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==, + } + engines: { node: ">=8" } path-key@4.0.0: - resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==, + } + engines: { node: ">=12" } path-parse@1.0.7: - resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + resolution: + { + integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==, + } path-scurry@1.11.1: - resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} - engines: {node: '>=16 || 14 >=14.18'} + resolution: + { + integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==, + } + engines: { node: ">=16 || 14 >=14.18" } path-scurry@2.0.2: - resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==} - engines: {node: 18 || 20 || >=22} + resolution: + { + integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==, + } + engines: { node: 18 || 20 || >=22 } path-to-regexp@6.3.0: - resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} + resolution: + { + integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==, + } pathe@2.0.3: - resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + resolution: + { + integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==, + } perfect-debounce@2.1.0: - resolution: {integrity: sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==} + resolution: + { + integrity: sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==, + } picocolors@1.1.1: - resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + resolution: + { + integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==, + } picomatch@2.3.2: - resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} - engines: {node: '>=8.6'} + resolution: + { + integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==, + } + engines: { node: ">=8.6" } picomatch@4.0.5: - resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==, + } + engines: { node: ">=12" } pify@2.3.0: - resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} - engines: {node: '>=0.10.0'} + resolution: + { + integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==, + } + engines: { node: ">=0.10.0" } pirates@4.0.7: - resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} - engines: {node: '>= 6'} + resolution: + { + integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==, + } + engines: { node: ">= 6" } pkg-types@1.3.1: - resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} + resolution: + { + integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==, + } pkg-types@2.3.1: - resolution: {integrity: sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==} + resolution: + { + integrity: sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==, + } portfinder@1.0.38: - resolution: {integrity: sha512-rEwq/ZHlJIKw++XtLAO8PPuOQA/zaPJOZJ37BVuN97nLpMJeuDVLVGRwbFoBgLudgdTMP2hdRJP++H+8QOA3vg==} - engines: {node: '>= 10.12'} + resolution: + { + integrity: sha512-rEwq/ZHlJIKw++XtLAO8PPuOQA/zaPJOZJ37BVuN97nLpMJeuDVLVGRwbFoBgLudgdTMP2hdRJP++H+8QOA3vg==, + } + engines: { node: ">= 10.12" } postcss-calc@10.1.1: - resolution: {integrity: sha512-NYEsLHh8DgG/PRH2+G9BTuUdtf9ViS+vdoQ0YA5OQdGsfN4ztiwtDWNtBl9EKeqNMFnIu8IKZ0cLxEQ5r5KVMw==} - engines: {node: ^18.12 || ^20.9 || >=22.0} + resolution: + { + integrity: sha512-NYEsLHh8DgG/PRH2+G9BTuUdtf9ViS+vdoQ0YA5OQdGsfN4ztiwtDWNtBl9EKeqNMFnIu8IKZ0cLxEQ5r5KVMw==, + } + engines: { node: ^18.12 || ^20.9 || >=22.0 } peerDependencies: postcss: ^8.4.38 postcss-colormin@8.0.2: - resolution: {integrity: sha512-3puH3etbn8GPaJuF8OybCdUW6PJO0KU9ZnsaA/1VG9HU0Wdrf94dOTQkbQnA8/qkYdPjwHzcWvzUGXIeawBa0w==} - engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} + resolution: + { + integrity: sha512-3puH3etbn8GPaJuF8OybCdUW6PJO0KU9ZnsaA/1VG9HU0Wdrf94dOTQkbQnA8/qkYdPjwHzcWvzUGXIeawBa0w==, + } + engines: { node: ^22.11.0 || ^24.11.0 || >=26.0 } peerDependencies: postcss: ^8.5.25 postcss-convert-values@8.0.2: - resolution: {integrity: sha512-KA6VVp93xASmDI0HWgRQ7938XR20hZEVL525YCbcTVsuxn4BNxs+ge4wLNow+ay+uqPuy3uEh51zhjfI92uN4w==} - engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} + resolution: + { + integrity: sha512-KA6VVp93xASmDI0HWgRQ7938XR20hZEVL525YCbcTVsuxn4BNxs+ge4wLNow+ay+uqPuy3uEh51zhjfI92uN4w==, + } + engines: { node: ^22.11.0 || ^24.11.0 || >=26.0 } peerDependencies: postcss: ^8.5.25 postcss-discard-comments@8.0.2: - resolution: {integrity: sha512-tQk36szZkG9ngZM9bKrUp3hM2SxdclYJVnMtAMKyi0RqY7IQpx67TJ3+0thRMV47niKBw9Y/1auGkBeCfPC97A==} - engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} + resolution: + { + integrity: sha512-tQk36szZkG9ngZM9bKrUp3hM2SxdclYJVnMtAMKyi0RqY7IQpx67TJ3+0thRMV47niKBw9Y/1auGkBeCfPC97A==, + } + engines: { node: ^22.11.0 || ^24.11.0 || >=26.0 } peerDependencies: postcss: ^8.5.25 postcss-discard-duplicates@8.0.2: - resolution: {integrity: sha512-Y2IDdRqvnpzsOH5ZLcJnvh/Eiye8O3r6uO7oFTe3YuFfrat3xQRDPjTVohRmi8RWuOIq2rGjva1n4Adus4pljw==} - engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} + resolution: + { + integrity: sha512-Y2IDdRqvnpzsOH5ZLcJnvh/Eiye8O3r6uO7oFTe3YuFfrat3xQRDPjTVohRmi8RWuOIq2rGjva1n4Adus4pljw==, + } + engines: { node: ^22.11.0 || ^24.11.0 || >=26.0 } peerDependencies: postcss: ^8.5.25 postcss-discard-empty@8.0.2: - resolution: {integrity: sha512-fGHTnqg2S4fBudyd+tCE+qT57R4YRUaYLannCrKYsE8u/rCMGCBMPwIM4jcNWJLP5vMEBitGrXa5mGB7VKA+Eg==} - engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} + resolution: + { + integrity: sha512-fGHTnqg2S4fBudyd+tCE+qT57R4YRUaYLannCrKYsE8u/rCMGCBMPwIM4jcNWJLP5vMEBitGrXa5mGB7VKA+Eg==, + } + engines: { node: ^22.11.0 || ^24.11.0 || >=26.0 } peerDependencies: postcss: ^8.5.25 postcss-discard-overridden@8.0.2: - resolution: {integrity: sha512-a9Dvv5ccOI7P94oCaeytj4FCSsHiXyH8KwXQorU/GJWDRvwFMY9yUs4uPKSG2HBSGtR49xo3ieml6F1EQCJtZA==} - engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} + resolution: + { + integrity: sha512-a9Dvv5ccOI7P94oCaeytj4FCSsHiXyH8KwXQorU/GJWDRvwFMY9yUs4uPKSG2HBSGtR49xo3ieml6F1EQCJtZA==, + } + engines: { node: ^22.11.0 || ^24.11.0 || >=26.0 } peerDependencies: postcss: ^8.5.25 postcss-import@15.1.0: - resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} - engines: {node: '>=14.0.0'} + resolution: + { + integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==, + } + engines: { node: ">=14.0.0" } peerDependencies: postcss: ^8.0.0 postcss-js@4.1.0: - resolution: {integrity: sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==} - engines: {node: ^12 || ^14 || >= 16} + resolution: + { + integrity: sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==, + } + engines: { node: ^12 || ^14 || >= 16 } peerDependencies: postcss: ^8.4.21 postcss-load-config@6.0.1: - resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==} - engines: {node: '>= 18'} + resolution: + { + integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==, + } + engines: { node: ">= 18" } peerDependencies: - jiti: '>=1.21.0' - postcss: '>=8.0.9' + jiti: ">=1.21.0" + postcss: ">=8.0.9" tsx: ^4.8.1 yaml: ^2.4.2 peerDependenciesMeta: @@ -3026,285 +4995,465 @@ packages: optional: true postcss-merge-longhand@8.0.2: - resolution: {integrity: sha512-iN1JQ21vKd3ZdbHmj69lqOwaWiFvxlXVQ5EeH/sw5M3TqI1sFR28G4yleAcNkMwB55E6qGRRVR3lem+ZJazmoQ==} - engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} + resolution: + { + integrity: sha512-iN1JQ21vKd3ZdbHmj69lqOwaWiFvxlXVQ5EeH/sw5M3TqI1sFR28G4yleAcNkMwB55E6qGRRVR3lem+ZJazmoQ==, + } + engines: { node: ^22.11.0 || ^24.11.0 || >=26.0 } peerDependencies: postcss: ^8.5.25 postcss-merge-rules@8.0.2: - resolution: {integrity: sha512-ipELN1m3k/GLGD+wmYHtiEVE2TMW5OKm4CgSRqYDeDz1ZX89AbeUCoeos0dUdTcpMilSa+zIRwvD78U3Uv/1BA==} - engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} + resolution: + { + integrity: sha512-ipELN1m3k/GLGD+wmYHtiEVE2TMW5OKm4CgSRqYDeDz1ZX89AbeUCoeos0dUdTcpMilSa+zIRwvD78U3Uv/1BA==, + } + engines: { node: ^22.11.0 || ^24.11.0 || >=26.0 } peerDependencies: postcss: ^8.5.25 postcss-minify-font-values@8.0.2: - resolution: {integrity: sha512-T6oURfdYH/BtXLLN/biomuY1hSYSGbfRyLyxQ+7+VCRw7Zj1ROpRwHpaX2PP/rpmT/0yHaGIVUNShmanvm3PXw==} - engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} + resolution: + { + integrity: sha512-T6oURfdYH/BtXLLN/biomuY1hSYSGbfRyLyxQ+7+VCRw7Zj1ROpRwHpaX2PP/rpmT/0yHaGIVUNShmanvm3PXw==, + } + engines: { node: ^22.11.0 || ^24.11.0 || >=26.0 } peerDependencies: postcss: ^8.5.25 postcss-minify-gradients@8.0.2: - resolution: {integrity: sha512-s49jAcFm5eF7GLLU0+28e4mQc/vISBMtu+1gQbq/Uf7+w155G8nbItaQp5NYY7QsYerpj2qvtNkYN25gZWWYdw==} - engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} + resolution: + { + integrity: sha512-s49jAcFm5eF7GLLU0+28e4mQc/vISBMtu+1gQbq/Uf7+w155G8nbItaQp5NYY7QsYerpj2qvtNkYN25gZWWYdw==, + } + engines: { node: ^22.11.0 || ^24.11.0 || >=26.0 } peerDependencies: postcss: ^8.5.25 postcss-minify-params@8.0.2: - resolution: {integrity: sha512-ifaU795JsBddkffWbTOXl8ubUyBqpNiLS8xjqKmE/Sw9AOpjWG2uFPfpEv3AZPXWNkWJf8kGvW9wLHImEUJUkA==} - engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} + resolution: + { + integrity: sha512-ifaU795JsBddkffWbTOXl8ubUyBqpNiLS8xjqKmE/Sw9AOpjWG2uFPfpEv3AZPXWNkWJf8kGvW9wLHImEUJUkA==, + } + engines: { node: ^22.11.0 || ^24.11.0 || >=26.0 } peerDependencies: postcss: ^8.5.25 postcss-minify-selectors@8.0.3: - resolution: {integrity: sha512-3WchKL9xoA80/jNkRcimRGIc9mP6sHPQUY/1WzV+GbF69I/upieyaKzZcIdQzf9cBmCJ6vjxi+NrKqCojoOb7Q==} - engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} + resolution: + { + integrity: sha512-3WchKL9xoA80/jNkRcimRGIc9mP6sHPQUY/1WzV+GbF69I/upieyaKzZcIdQzf9cBmCJ6vjxi+NrKqCojoOb7Q==, + } + engines: { node: ^22.11.0 || ^24.11.0 || >=26.0 } peerDependencies: postcss: ^8.5.25 postcss-nested@6.2.0: - resolution: {integrity: sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==} - engines: {node: '>=12.0'} + resolution: + { + integrity: sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==, + } + engines: { node: ">=12.0" } peerDependencies: postcss: ^8.2.14 postcss-nesting@13.0.2: - resolution: {integrity: sha512-1YCI290TX+VP0U/K/aFxzHzQWHWURL+CtHMSbex1lCdpXD1SoR2sYuxDu5aNI9lPoXpKTCggFZiDJbwylU0LEQ==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-1YCI290TX+VP0U/K/aFxzHzQWHWURL+CtHMSbex1lCdpXD1SoR2sYuxDu5aNI9lPoXpKTCggFZiDJbwylU0LEQ==, + } + engines: { node: ">=18" } peerDependencies: postcss: ^8.4 postcss-normalize-charset@8.0.2: - resolution: {integrity: sha512-iy3/b+gX+dHfbNZq/4rfThbMAqxhneBJEhS71y5toliav5rLowkZ6g0ZJGymXRZytDOt/u+fRSFkNJLkRPYaug==} - engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} + resolution: + { + integrity: sha512-iy3/b+gX+dHfbNZq/4rfThbMAqxhneBJEhS71y5toliav5rLowkZ6g0ZJGymXRZytDOt/u+fRSFkNJLkRPYaug==, + } + engines: { node: ^22.11.0 || ^24.11.0 || >=26.0 } peerDependencies: postcss: ^8.5.25 postcss-normalize-display-values@8.0.2: - resolution: {integrity: sha512-T6Az9mgc7FUWvI6mK0uOsItT6csep7fdLtDSRC2XBMTCvrQmtYiz86T5hEM6sfAkZVVhfrALPhdPxJWVoNAktw==} - engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} + resolution: + { + integrity: sha512-T6Az9mgc7FUWvI6mK0uOsItT6csep7fdLtDSRC2XBMTCvrQmtYiz86T5hEM6sfAkZVVhfrALPhdPxJWVoNAktw==, + } + engines: { node: ^22.11.0 || ^24.11.0 || >=26.0 } peerDependencies: postcss: ^8.5.25 postcss-normalize-positions@8.0.2: - resolution: {integrity: sha512-BBg188AxYLC86LDEnkbQTcdnZugP2vDwYQQDV+Htju2gpTqM2a3o/oQ91/h9+j0277InjNgrI7l1LDDRrlWQ5w==} - engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} + resolution: + { + integrity: sha512-BBg188AxYLC86LDEnkbQTcdnZugP2vDwYQQDV+Htju2gpTqM2a3o/oQ91/h9+j0277InjNgrI7l1LDDRrlWQ5w==, + } + engines: { node: ^22.11.0 || ^24.11.0 || >=26.0 } peerDependencies: postcss: ^8.5.25 postcss-normalize-repeat-style@8.0.2: - resolution: {integrity: sha512-6mbVJzuogwcAHMd6MMpmAMcc3BkleOcY2xazCp+HCMiod78tx7rdOrufTrPYGjBLaTjkgNUr8KCkLHj8mfH6WQ==} - engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} + resolution: + { + integrity: sha512-6mbVJzuogwcAHMd6MMpmAMcc3BkleOcY2xazCp+HCMiod78tx7rdOrufTrPYGjBLaTjkgNUr8KCkLHj8mfH6WQ==, + } + engines: { node: ^22.11.0 || ^24.11.0 || >=26.0 } peerDependencies: postcss: ^8.5.25 postcss-normalize-string@8.0.2: - resolution: {integrity: sha512-8UK2KJSChlv7V12QgWTWtbRzzZcki/f21e8egMlFHTE9rEBCCyLo60rcY3mh77cDZhrBSIfKM9ciOsU6249nbA==} - engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} + resolution: + { + integrity: sha512-8UK2KJSChlv7V12QgWTWtbRzzZcki/f21e8egMlFHTE9rEBCCyLo60rcY3mh77cDZhrBSIfKM9ciOsU6249nbA==, + } + engines: { node: ^22.11.0 || ^24.11.0 || >=26.0 } peerDependencies: postcss: ^8.5.25 postcss-normalize-timing-functions@8.0.2: - resolution: {integrity: sha512-C2OyYiEMKjCxA0m+QJ5SeMf12OenIIGqrXEuWizEunZZP+s5TRq1aT8naxqr/8rytSRqD4SwH4mywc9G44u4cw==} - engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} + resolution: + { + integrity: sha512-C2OyYiEMKjCxA0m+QJ5SeMf12OenIIGqrXEuWizEunZZP+s5TRq1aT8naxqr/8rytSRqD4SwH4mywc9G44u4cw==, + } + engines: { node: ^22.11.0 || ^24.11.0 || >=26.0 } peerDependencies: postcss: ^8.5.25 postcss-normalize-unicode@8.0.2: - resolution: {integrity: sha512-ttWq9oM88gSPpfspoZB49r/lv1fvjOadi79E/4BxGVt0rughnNDdTnDLmWj+epOjS6iKhmse3kbNcISR6Jl+3A==} - engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} + resolution: + { + integrity: sha512-ttWq9oM88gSPpfspoZB49r/lv1fvjOadi79E/4BxGVt0rughnNDdTnDLmWj+epOjS6iKhmse3kbNcISR6Jl+3A==, + } + engines: { node: ^22.11.0 || ^24.11.0 || >=26.0 } peerDependencies: postcss: ^8.5.25 postcss-normalize-url@8.0.2: - resolution: {integrity: sha512-PvSiaJOQpP0AW5GSHrWZXupfhUgLUtS1MubJX+wPwh6RZZP6CHnZQy/S8uX2vvHMd57qIZ44Sb/37rFMiAWPfA==} - engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} + resolution: + { + integrity: sha512-PvSiaJOQpP0AW5GSHrWZXupfhUgLUtS1MubJX+wPwh6RZZP6CHnZQy/S8uX2vvHMd57qIZ44Sb/37rFMiAWPfA==, + } + engines: { node: ^22.11.0 || ^24.11.0 || >=26.0 } peerDependencies: postcss: ^8.5.25 postcss-normalize-whitespace@8.0.2: - resolution: {integrity: sha512-Fipz8bjy96XPmtMPsqbGw3fypbS+aOHJfpTzfQY9z0lDYFqqGIqYWwrfsi2ZOv7B86pY81vQGJ6mSHWvTXDV7g==} - engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} + resolution: + { + integrity: sha512-Fipz8bjy96XPmtMPsqbGw3fypbS+aOHJfpTzfQY9z0lDYFqqGIqYWwrfsi2ZOv7B86pY81vQGJ6mSHWvTXDV7g==, + } + engines: { node: ^22.11.0 || ^24.11.0 || >=26.0 } peerDependencies: postcss: ^8.5.25 postcss-ordered-values@8.0.2: - resolution: {integrity: sha512-LBqMGd6Bam4TVp9dKui01o0prWtsGfPP+WtENunNs3L9oi+t75uPyEmzzaimxinmGgeiFLix9xLI0FfD+qedjw==} - engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} + resolution: + { + integrity: sha512-LBqMGd6Bam4TVp9dKui01o0prWtsGfPP+WtENunNs3L9oi+t75uPyEmzzaimxinmGgeiFLix9xLI0FfD+qedjw==, + } + engines: { node: ^22.11.0 || ^24.11.0 || >=26.0 } peerDependencies: postcss: ^8.5.25 postcss-reduce-initial@8.0.2: - resolution: {integrity: sha512-+612lhSpyp1g4ZU0zQKwmdHnAi8LevMpzvQuP2RVhq+EUWqAwwUMk03OQEfzy+es9OSdRTuy5ugnFeEeOiIe2A==} - engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} + resolution: + { + integrity: sha512-+612lhSpyp1g4ZU0zQKwmdHnAi8LevMpzvQuP2RVhq+EUWqAwwUMk03OQEfzy+es9OSdRTuy5ugnFeEeOiIe2A==, + } + engines: { node: ^22.11.0 || ^24.11.0 || >=26.0 } peerDependencies: postcss: ^8.5.25 postcss-reduce-transforms@8.0.2: - resolution: {integrity: sha512-7D2vDXJ2HBNQpbiw5dY9UyLfBRCGRBjV0ChMqHdHghFsjAH3hIIUgb/rE+e77LFiT8VjXUK2fZh3omG4R96n8A==} - engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} + resolution: + { + integrity: sha512-7D2vDXJ2HBNQpbiw5dY9UyLfBRCGRBjV0ChMqHdHghFsjAH3hIIUgb/rE+e77LFiT8VjXUK2fZh3omG4R96n8A==, + } + engines: { node: ^22.11.0 || ^24.11.0 || >=26.0 } peerDependencies: postcss: ^8.5.25 postcss-selector-parser@6.0.10: - resolution: {integrity: sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==} - engines: {node: '>=4'} + resolution: + { + integrity: sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==, + } + engines: { node: ">=4" } postcss-selector-parser@6.1.2: - resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==} - engines: {node: '>=4'} + resolution: + { + integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==, + } + engines: { node: ">=4" } postcss-selector-parser@7.1.4: - resolution: {integrity: sha512-HeP7D2wyhkR+XaK6v4W8oRF62Dsz4flyuczALJp61GckGm42u1saSSJ/0auvcBqxs3jMRFEcPK34At/0JBKdOg==} - engines: {node: '>=4'} + resolution: + { + integrity: sha512-HeP7D2wyhkR+XaK6v4W8oRF62Dsz4flyuczALJp61GckGm42u1saSSJ/0auvcBqxs3jMRFEcPK34At/0JBKdOg==, + } + engines: { node: ">=4" } postcss-selector-parser@7.1.5: - resolution: {integrity: sha512-KvvtD7SrlBP7dlgkBghEE3r84CABm5SmV2aNcG4oCA+qDnJ/tvKonFVvwWAyyWUEwxuNawdfEAZKP9zM3oZ2Uw==} - engines: {node: '>=4'} + resolution: + { + integrity: sha512-KvvtD7SrlBP7dlgkBghEE3r84CABm5SmV2aNcG4oCA+qDnJ/tvKonFVvwWAyyWUEwxuNawdfEAZKP9zM3oZ2Uw==, + } + engines: { node: ">=4" } postcss-svgo@8.0.3: - resolution: {integrity: sha512-ADG8YNtwE5bcqvxw1gU0X7FkgdvinZZxWKHSCMwayX7gPl4XRmBMyiC84Ukal5bPS9Nk/2tI7siJwqxIN4/grg==} - engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} + resolution: + { + integrity: sha512-ADG8YNtwE5bcqvxw1gU0X7FkgdvinZZxWKHSCMwayX7gPl4XRmBMyiC84Ukal5bPS9Nk/2tI7siJwqxIN4/grg==, + } + engines: { node: ^22.11.0 || ^24.11.0 || >=26.0 } peerDependencies: postcss: ^8.5.25 postcss-unique-selectors@8.0.2: - resolution: {integrity: sha512-cIftRB4rW3UCqnQjFAS6WlUzragjsU2AtMzWDdDQKTMjgPh1rO8qYgnoYqkUgRXKnxd3902laOWbkkYO/GYEHw==} - engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} + resolution: + { + integrity: sha512-cIftRB4rW3UCqnQjFAS6WlUzragjsU2AtMzWDdDQKTMjgPh1rO8qYgnoYqkUgRXKnxd3902laOWbkkYO/GYEHw==, + } + engines: { node: ^22.11.0 || ^24.11.0 || >=26.0 } peerDependencies: postcss: ^8.5.25 postcss-value-parser@4.2.0: - resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + resolution: + { + integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==, + } postcss@8.5.24: - resolution: {integrity: sha512-8RyVklq0owXUTa4xlpzu4l9AaVKIdQvAcOHZWaMh98HgySsUtxRVf/chRe3dsSLqb6i40BzGRzEUddRaI+9TSw==} - engines: {node: ^10 || ^12 || >=14} + resolution: + { + integrity: sha512-8RyVklq0owXUTa4xlpzu4l9AaVKIdQvAcOHZWaMh98HgySsUtxRVf/chRe3dsSLqb6i40BzGRzEUddRaI+9TSw==, + } + engines: { node: ^10 || ^12 || >=14 } postcss@8.5.26: - resolution: {integrity: sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==} - engines: {node: ^10 || ^12 || >=14} + resolution: + { + integrity: sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==, + } + engines: { node: ^10 || ^12 || >=14 } powershell-utils@0.1.0: - resolution: {integrity: sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==} - engines: {node: '>=20'} + resolution: + { + integrity: sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==, + } + engines: { node: ">=20" } pretty-bytes@7.1.1: - resolution: {integrity: sha512-X+vn9z8nOFZQlxOLmfJ0iKDdMD7jYTsTW12OAlCpdoE3Igik6L37pugIZi+N3usuyp5McfgKPWi12q3zvHLeGQ==} - engines: {node: '>=20'} + resolution: + { + integrity: sha512-X+vn9z8nOFZQlxOLmfJ0iKDdMD7jYTsTW12OAlCpdoE3Igik6L37pugIZi+N3usuyp5McfgKPWi12q3zvHLeGQ==, + } + engines: { node: ">=20" } process-nextick-args@2.0.1: - resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + resolution: + { + integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==, + } process@0.11.10: - resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} - engines: {node: '>= 0.6.0'} + resolution: + { + integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==, + } + engines: { node: ">= 0.6.0" } proper-lockfile@4.1.2: - resolution: {integrity: sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==} + resolution: + { + integrity: sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==, + } punycode.js@2.3.1: - resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} - engines: {node: '>=6'} + resolution: + { + integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==, + } + engines: { node: ">=6" } quansync@0.2.11: - resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} + resolution: + { + integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==, + } queue-microtask@1.2.3: - resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + resolution: + { + integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==, + } radix3@1.1.2: - resolution: {integrity: sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==} + resolution: + { + integrity: sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==, + } range-parser@1.3.0: - resolution: {integrity: sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==} - engines: {node: '>= 0.6'} + resolution: + { + integrity: sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==, + } + engines: { node: ">= 0.6" } rc9@2.1.2: - resolution: {integrity: sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==} + resolution: + { + integrity: sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==, + } rc9@3.0.1: - resolution: {integrity: sha512-gMDyleLWVE+i6Sgtc0QbbY6pEKqYs97NGi6isHQPqYlLemPoO8dxQ3uGi0f4NiP98c+jMW6cG1Kx9dDwfvqARQ==} + resolution: + { + integrity: sha512-gMDyleLWVE+i6Sgtc0QbbY6pEKqYs97NGi6isHQPqYlLemPoO8dxQ3uGi0f4NiP98c+jMW6cG1Kx9dDwfvqARQ==, + } read-cache@1.0.0: - resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} + resolution: + { + integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==, + } readable-stream@2.3.8: - resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + resolution: + { + integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==, + } readable-stream@4.7.0: - resolution: {integrity: sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + resolution: + { + integrity: sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==, + } + engines: { node: ^12.22.0 || ^14.17.0 || >=16.0.0 } readdir-glob@1.1.3: - resolution: {integrity: sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==} + resolution: + { + integrity: sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==, + } readdirp@3.6.0: - resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} - engines: {node: '>=8.10.0'} + resolution: + { + integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==, + } + engines: { node: ">=8.10.0" } readdirp@4.1.2: - resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} - engines: {node: '>= 14.18.0'} + resolution: + { + integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==, + } + engines: { node: ">= 14.18.0" } readdirp@5.1.1: - resolution: {integrity: sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA==} - engines: {node: '>= 20.19.0'} + resolution: + { + integrity: sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA==, + } + engines: { node: ">= 20.19.0" } redis-errors@1.2.0: - resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==} - engines: {node: '>=4'} + resolution: + { + integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==, + } + engines: { node: ">=4" } redis-parser@3.0.0: - resolution: {integrity: sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==} - engines: {node: '>=4'} + resolution: + { + integrity: sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==, + } + engines: { node: ">=4" } replace-in-file@6.3.5: - resolution: {integrity: sha512-arB9d3ENdKva2fxRnSjwBEXfK1npgyci7ZZuwysgAp7ORjHSyxz6oqIjTEv8R0Ydl4Ll7uOAZXL4vbkhGIizCg==} - engines: {node: '>=10'} + resolution: + { + integrity: sha512-arB9d3ENdKva2fxRnSjwBEXfK1npgyci7ZZuwysgAp7ORjHSyxz6oqIjTEv8R0Ydl4Ll7uOAZXL4vbkhGIizCg==, + } + engines: { node: ">=10" } hasBin: true require-directory@2.1.1: - resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} - engines: {node: '>=0.10.0'} + resolution: + { + integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==, + } + engines: { node: ">=0.10.0" } resolve-from@5.0.0: - resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==, + } + engines: { node: ">=8" } resolve-path@1.4.0: - resolution: {integrity: sha512-i1xevIst/Qa+nA9olDxLWnLk8YZbi8R/7JPbCMcgyWaFR6bKWaexgJgEB5oc2PKMjYdrHynyz0NY+if+H98t1w==} - engines: {node: '>= 0.8'} + resolution: + { + integrity: sha512-i1xevIst/Qa+nA9olDxLWnLk8YZbi8R/7JPbCMcgyWaFR6bKWaexgJgEB5oc2PKMjYdrHynyz0NY+if+H98t1w==, + } + engines: { node: ">= 0.8" } resolve@1.22.12: - resolution: {integrity: sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==, + } + engines: { node: ">= 0.4" } hasBin: true retry@0.12.0: - resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} - engines: {node: '>= 4'} + resolution: + { + integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==, + } + engines: { node: ">= 4" } reusify@1.1.0: - resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} - engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + resolution: + { + integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==, + } + engines: { iojs: ">=1.0.0", node: ">=0.10.0" } rolldown-string@0.3.1: - resolution: {integrity: sha512-dv8GOXkYqUQdI0rsXB8hmzO95pKWSuX2eg5/JSJa9czfEVIFuKNR7ZJDfat8LlmD35RAhWY+evS7/wXXqFDfSg==} - engines: {node: '>=20.19.0'} + resolution: + { + integrity: sha512-dv8GOXkYqUQdI0rsXB8hmzO95pKWSuX2eg5/JSJa9czfEVIFuKNR7ZJDfat8LlmD35RAhWY+evS7/wXXqFDfSg==, + } + engines: { node: ">=20.19.0" } peerDependencies: - rolldown: '*' + rolldown: "*" peerDependenciesMeta: rolldown: optional: true rolldown@1.2.3: - resolution: {integrity: sha512-rn9wpmxplLf7NLNyCk9FyWh3FM43DbY8jOzCdEPzH7uflhTftRbCEpqi6Ly2osgoU8OwObtmavMbWLaWy4LX7A==} - engines: {node: ^20.19.0 || >=22.12.0} + resolution: + { + integrity: sha512-rn9wpmxplLf7NLNyCk9FyWh3FM43DbY8jOzCdEPzH7uflhTftRbCEpqi6Ly2osgoU8OwObtmavMbWLaWy4LX7A==, + } + engines: { node: ^20.19.0 || >=22.12.0 } hasBin: true rollup-plugin-visualizer@7.0.1: - resolution: {integrity: sha512-UJUT4+1Ho4OcWmPYU3sYXgUqI8B8Ayfe06MX7y0qCJ1K8aGoKtR/NDd/2nZqM7ADkrzny+I99Ul7GgyoiVNAgg==} - engines: {node: '>=22'} + resolution: + { + integrity: sha512-UJUT4+1Ho4OcWmPYU3sYXgUqI8B8Ayfe06MX7y0qCJ1K8aGoKtR/NDd/2nZqM7ADkrzny+I99Ul7GgyoiVNAgg==, + } + engines: { node: ">=22" } hasBin: true peerDependencies: rolldown: 1.x || ^1.0.0-beta || ^1.0.0-rc @@ -3316,471 +5465,804 @@ packages: optional: true rollup@4.62.4: - resolution: {integrity: sha512-RXOqwaPsBGjMNMa4sQjDjHieHEZDFoj/Rdr46l2MU5DfEs16wHJPC2RPTPHWhNl+M3aI472LLqFkFKut4SblOg==} - engines: {node: '>=18.0.0', npm: '>=8.0.0'} + resolution: + { + integrity: sha512-RXOqwaPsBGjMNMa4sQjDjHieHEZDFoj/Rdr46l2MU5DfEs16wHJPC2RPTPHWhNl+M3aI472LLqFkFKut4SblOg==, + } + engines: { node: ">=18.0.0", npm: ">=8.0.0" } hasBin: true rou3@0.9.1: - resolution: {integrity: sha512-z/sSmzvtwMDDnxsPVhfWMuG6F6mbmhFDXoVqLmMfbpDD9qfV3GDmSQpf0+W296/ZDIpW2wcMmBfpVFzcnOi/nA==} + resolution: + { + integrity: sha512-z/sSmzvtwMDDnxsPVhfWMuG6F6mbmhFDXoVqLmMfbpDD9qfV3GDmSQpf0+W296/ZDIpW2wcMmBfpVFzcnOi/nA==, + } run-applescript@7.1.0: - resolution: {integrity: sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==, + } + engines: { node: ">=18" } run-parallel@1.2.0: - resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + resolution: + { + integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==, + } rxjs@7.8.2: - resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} + resolution: + { + integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==, + } safe-buffer@5.1.2: - resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + resolution: + { + integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==, + } safe-buffer@5.2.1: - resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + resolution: + { + integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==, + } safe-regex-test@1.1.0: - resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==, + } + engines: { node: ">= 0.4" } sass-embedded-all-unknown@1.93.3: - resolution: {integrity: sha512-3okGgnE41eg+CPLtAPletu6nQ4N0ij7AeW+Sl5Km4j29XcmqZQeFwYjHe1AlKTEgLi/UAONk1O8i8/lupeKMbw==} - cpu: ['!arm', '!arm64', '!riscv64', '!x64'] + resolution: + { + integrity: sha512-3okGgnE41eg+CPLtAPletu6nQ4N0ij7AeW+Sl5Km4j29XcmqZQeFwYjHe1AlKTEgLi/UAONk1O8i8/lupeKMbw==, + } + cpu: ["!arm", "!arm64", "!riscv64", "!x64"] sass-embedded-android-arm64@1.93.3: - resolution: {integrity: sha512-uqUl3Kt1IqdGVAcAdbmC+NwuUJy8tM+2ZnB7/zrt6WxWVShVCRdFnWR9LT8HJr7eJN7AU8kSXxaVX/gedanPsg==} - engines: {node: '>=14.0.0'} + resolution: + { + integrity: sha512-uqUl3Kt1IqdGVAcAdbmC+NwuUJy8tM+2ZnB7/zrt6WxWVShVCRdFnWR9LT8HJr7eJN7AU8kSXxaVX/gedanPsg==, + } + engines: { node: ">=14.0.0" } cpu: [arm64] os: [android] sass-embedded-android-arm@1.93.3: - resolution: {integrity: sha512-8xOw9bywfOD6Wv24BgCmgjkk6tMrsOTTHcb28KDxeJtFtoxiUyMbxo0vChpPAfp2Hyg2tFFKS60s0s4JYk+Raw==} - engines: {node: '>=14.0.0'} + resolution: + { + integrity: sha512-8xOw9bywfOD6Wv24BgCmgjkk6tMrsOTTHcb28KDxeJtFtoxiUyMbxo0vChpPAfp2Hyg2tFFKS60s0s4JYk+Raw==, + } + engines: { node: ">=14.0.0" } cpu: [arm] os: [android] sass-embedded-android-riscv64@1.93.3: - resolution: {integrity: sha512-2jNJDmo+3qLocjWqYbXiBDnfgwrUeZgZFHJIwAefU7Fn66Ot7rsXl+XPwlokaCbTpj7eMFIqsRAZ/uDueXNCJg==} - engines: {node: '>=14.0.0'} + resolution: + { + integrity: sha512-2jNJDmo+3qLocjWqYbXiBDnfgwrUeZgZFHJIwAefU7Fn66Ot7rsXl+XPwlokaCbTpj7eMFIqsRAZ/uDueXNCJg==, + } + engines: { node: ">=14.0.0" } cpu: [riscv64] os: [android] sass-embedded-android-x64@1.93.3: - resolution: {integrity: sha512-y0RoAU6ZenQFcjM9PjQd3cRqRTjqwSbtWLL/p68y2oFyh0QGN0+LQ826fc0ZvU/AbqCsAizkqjzOn6cRZJxTTQ==} - engines: {node: '>=14.0.0'} + resolution: + { + integrity: sha512-y0RoAU6ZenQFcjM9PjQd3cRqRTjqwSbtWLL/p68y2oFyh0QGN0+LQ826fc0ZvU/AbqCsAizkqjzOn6cRZJxTTQ==, + } + engines: { node: ">=14.0.0" } cpu: [x64] os: [android] sass-embedded-darwin-arm64@1.93.3: - resolution: {integrity: sha512-7zb/hpdMOdKteK17BOyyypemglVURd1Hdz6QGsggy60aUFfptTLQftLRg8r/xh1RbQAUKWFbYTNaM47J9yPxYg==} - engines: {node: '>=14.0.0'} + resolution: + { + integrity: sha512-7zb/hpdMOdKteK17BOyyypemglVURd1Hdz6QGsggy60aUFfptTLQftLRg8r/xh1RbQAUKWFbYTNaM47J9yPxYg==, + } + engines: { node: ">=14.0.0" } cpu: [arm64] os: [darwin] sass-embedded-darwin-x64@1.93.3: - resolution: {integrity: sha512-Ek1Vp8ZDQEe327Lz0b7h3hjvWH3u9XjJiQzveq74RPpJQ2q6d9LfWpjiRRohM4qK6o4XOHw1X10OMWPXJtdtWg==} - engines: {node: '>=14.0.0'} + resolution: + { + integrity: sha512-Ek1Vp8ZDQEe327Lz0b7h3hjvWH3u9XjJiQzveq74RPpJQ2q6d9LfWpjiRRohM4qK6o4XOHw1X10OMWPXJtdtWg==, + } + engines: { node: ">=14.0.0" } cpu: [x64] os: [darwin] sass-embedded-linux-arm64@1.93.3: - resolution: {integrity: sha512-RBrHWgfd8Dd8w4fbmdRVXRrhh8oBAPyeWDTKAWw8ZEmuXfVl4ytjDuyxaVilh6rR1xTRTNpbaA/YWApBlLrrNw==} - engines: {node: '>=14.0.0'} + resolution: + { + integrity: sha512-RBrHWgfd8Dd8w4fbmdRVXRrhh8oBAPyeWDTKAWw8ZEmuXfVl4ytjDuyxaVilh6rR1xTRTNpbaA/YWApBlLrrNw==, + } + engines: { node: ">=14.0.0" } cpu: [arm64] os: [linux] libc: glibc sass-embedded-linux-arm@1.93.3: - resolution: {integrity: sha512-yeiv2y+dp8B4wNpd3+JsHYD0mvpXSfov7IGyQ1tMIR40qv+ROkRqYiqQvAOXf76Qwh4Y9OaYZtLpnsPjfeq6mA==} - engines: {node: '>=14.0.0'} + resolution: + { + integrity: sha512-yeiv2y+dp8B4wNpd3+JsHYD0mvpXSfov7IGyQ1tMIR40qv+ROkRqYiqQvAOXf76Qwh4Y9OaYZtLpnsPjfeq6mA==, + } + engines: { node: ">=14.0.0" } cpu: [arm] os: [linux] libc: glibc sass-embedded-linux-musl-arm64@1.93.3: - resolution: {integrity: sha512-PS829l+eUng+9W4PFclXGb4uA2+965NHV3/Sa5U7qTywjeeUUYTZg70dJHSqvhrBEfCc2XJABeW3adLJbyQYkw==} - engines: {node: '>=14.0.0'} + resolution: + { + integrity: sha512-PS829l+eUng+9W4PFclXGb4uA2+965NHV3/Sa5U7qTywjeeUUYTZg70dJHSqvhrBEfCc2XJABeW3adLJbyQYkw==, + } + engines: { node: ">=14.0.0" } cpu: [arm64] os: [linux] libc: musl sass-embedded-linux-musl-arm@1.93.3: - resolution: {integrity: sha512-fU0fwAwbp7sBE3h5DVU5UPzvaLg7a4yONfFWkkcCp6ZrOiPuGRHXXYriWQ0TUnWy4wE+svsVuWhwWgvlb/tkKg==} - engines: {node: '>=14.0.0'} + resolution: + { + integrity: sha512-fU0fwAwbp7sBE3h5DVU5UPzvaLg7a4yONfFWkkcCp6ZrOiPuGRHXXYriWQ0TUnWy4wE+svsVuWhwWgvlb/tkKg==, + } + engines: { node: ">=14.0.0" } cpu: [arm] os: [linux] libc: musl sass-embedded-linux-musl-riscv64@1.93.3: - resolution: {integrity: sha512-cK1oBY+FWQquaIGEeQ5H74KTO8cWsSWwXb/WaildOO9U6wmUypTgUYKQ0o5o/29nZbWWlM1PHuwVYTSnT23Jjg==} - engines: {node: '>=14.0.0'} + resolution: + { + integrity: sha512-cK1oBY+FWQquaIGEeQ5H74KTO8cWsSWwXb/WaildOO9U6wmUypTgUYKQ0o5o/29nZbWWlM1PHuwVYTSnT23Jjg==, + } + engines: { node: ">=14.0.0" } cpu: [riscv64] os: [linux] libc: musl sass-embedded-linux-musl-x64@1.93.3: - resolution: {integrity: sha512-A7wkrsHu2/I4Zpa0NMuPGkWDVV7QGGytxGyUq3opSXgAexHo/vBPlGoDXoRlSdex0cV+aTMRPjoGIfdmNlHwyg==} - engines: {node: '>=14.0.0'} + resolution: + { + integrity: sha512-A7wkrsHu2/I4Zpa0NMuPGkWDVV7QGGytxGyUq3opSXgAexHo/vBPlGoDXoRlSdex0cV+aTMRPjoGIfdmNlHwyg==, + } + engines: { node: ">=14.0.0" } cpu: [x64] os: [linux] libc: musl sass-embedded-linux-riscv64@1.93.3: - resolution: {integrity: sha512-vWkW1+HTF5qcaHa6hO80gx/QfB6GGjJUP0xLbnAoY4pwEnw5ulGv6RM8qYr8IDhWfVt/KH+lhJ2ZFxnJareisQ==} - engines: {node: '>=14.0.0'} + resolution: + { + integrity: sha512-vWkW1+HTF5qcaHa6hO80gx/QfB6GGjJUP0xLbnAoY4pwEnw5ulGv6RM8qYr8IDhWfVt/KH+lhJ2ZFxnJareisQ==, + } + engines: { node: ">=14.0.0" } cpu: [riscv64] os: [linux] libc: glibc sass-embedded-linux-x64@1.93.3: - resolution: {integrity: sha512-k6uFxs+e5jSuk1Y0niCwuq42F9ZC5UEP7P+RIOurIm8w/5QFa0+YqeW+BPWEW5M1FqVOsNZH3qGn4ahqvAEjPA==} - engines: {node: '>=14.0.0'} + resolution: + { + integrity: sha512-k6uFxs+e5jSuk1Y0niCwuq42F9ZC5UEP7P+RIOurIm8w/5QFa0+YqeW+BPWEW5M1FqVOsNZH3qGn4ahqvAEjPA==, + } + engines: { node: ">=14.0.0" } cpu: [x64] os: [linux] libc: glibc sass-embedded-unknown-all@1.93.3: - resolution: {integrity: sha512-o5wj2rLpXH0C+GJKt/VpWp6AnMsCCbfFmnMAttcrsa+U3yrs/guhZ3x55KAqqUsE8F47e3frbsDL+1OuQM5DAA==} - os: ['!android', '!darwin', '!linux', '!win32'] + resolution: + { + integrity: sha512-o5wj2rLpXH0C+GJKt/VpWp6AnMsCCbfFmnMAttcrsa+U3yrs/guhZ3x55KAqqUsE8F47e3frbsDL+1OuQM5DAA==, + } + os: ["!android", "!darwin", "!linux", "!win32"] sass-embedded-win32-arm64@1.93.3: - resolution: {integrity: sha512-0dOfT9moy9YmBolodwYYXtLwNr4jL4HQC9rBfv6mVrD7ud8ue2kDbn+GVzj1hEJxvEexVSmDCf7MHUTLcGs9xQ==} - engines: {node: '>=14.0.0'} + resolution: + { + integrity: sha512-0dOfT9moy9YmBolodwYYXtLwNr4jL4HQC9rBfv6mVrD7ud8ue2kDbn+GVzj1hEJxvEexVSmDCf7MHUTLcGs9xQ==, + } + engines: { node: ">=14.0.0" } cpu: [arm64] os: [win32] sass-embedded-win32-x64@1.93.3: - resolution: {integrity: sha512-wHFVfxiS9hU/sNk7KReD+lJWRp3R0SLQEX4zfOnRP2zlvI2X4IQR5aZr9GNcuMP6TmNpX0nQPZTegS8+h9RrEg==} - engines: {node: '>=14.0.0'} + resolution: + { + integrity: sha512-wHFVfxiS9hU/sNk7KReD+lJWRp3R0SLQEX4zfOnRP2zlvI2X4IQR5aZr9GNcuMP6TmNpX0nQPZTegS8+h9RrEg==, + } + engines: { node: ">=14.0.0" } cpu: [x64] os: [win32] sass-embedded@1.93.3: - resolution: {integrity: sha512-+VUy01yfDqNmIVMd/LLKl2TTtY0ovZN0rTonh+FhKr65mFwIYgU9WzgIZKS7U9/SPCQvWTsTGx9jyt+qRm/XFw==} - engines: {node: '>=16.0.0'} + resolution: + { + integrity: sha512-+VUy01yfDqNmIVMd/LLKl2TTtY0ovZN0rTonh+FhKr65mFwIYgU9WzgIZKS7U9/SPCQvWTsTGx9jyt+qRm/XFw==, + } + engines: { node: ">=16.0.0" } hasBin: true sass@1.93.3: - resolution: {integrity: sha512-elOcIZRTM76dvxNAjqYrucTSI0teAF/L2Lv0s6f6b7FOwcwIuA357bIE871580AjHJuSvLIRUosgV+lIWx6Rgg==} - engines: {node: '>=14.0.0'} + resolution: + { + integrity: sha512-elOcIZRTM76dvxNAjqYrucTSI0teAF/L2Lv0s6f6b7FOwcwIuA357bIE871580AjHJuSvLIRUosgV+lIWx6Rgg==, + } + engines: { node: ">=14.0.0" } hasBin: true sax@1.6.1: - resolution: {integrity: sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q==} - engines: {node: '>=11.0.0'} + resolution: + { + integrity: sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q==, + } + engines: { node: ">=11.0.0" } scss@0.2.4: - resolution: {integrity: sha512-4u8V87F+Q/upVhUmhPnB4C1R11xojkRkWjExL2v0CX2EXTg18VrKd+9JWoeyCp2VEMdSpJsyAvVU+rVjogh51A==} - engines: {node: '>= 0.2.0'} + resolution: + { + integrity: sha512-4u8V87F+Q/upVhUmhPnB4C1R11xojkRkWjExL2v0CX2EXTg18VrKd+9JWoeyCp2VEMdSpJsyAvVU+rVjogh51A==, + } + engines: { node: ">= 0.2.0" } scule@1.3.0: - resolution: {integrity: sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==} + resolution: + { + integrity: sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==, + } semver@6.3.1: - resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + resolution: + { + integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==, + } hasBin: true semver@7.8.5: - resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} - engines: {node: '>=10'} + resolution: + { + integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==, + } + engines: { node: ">=10" } hasBin: true send@1.2.1: - resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} - engines: {node: '>= 18'} + resolution: + { + integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==, + } + engines: { node: ">= 18" } serialize-javascript@7.0.7: - resolution: {integrity: sha512-YAy8Od6KV+uuwUuU50np8fGB/Aues6Y0nAhA9y/hId74PlKUcme4pXcBD46NWKr1Q4osN/iseZ17YqO1XfmI8g==} - engines: {node: '>=20.0.0'} + resolution: + { + integrity: sha512-YAy8Od6KV+uuwUuU50np8fGB/Aues6Y0nAhA9y/hId74PlKUcme4pXcBD46NWKr1Q4osN/iseZ17YqO1XfmI8g==, + } + engines: { node: ">=20.0.0" } seroval@1.6.2: - resolution: {integrity: sha512-mPT+SD2TrlB6wvte1KkYOYUkubaTbd6pZ/6Kk3C9nxzrHmCZyhxOO7XGAeL7f+yLKZglzGtM9odUVvg/EhO+vQ==} - engines: {node: '>=10'} + resolution: + { + integrity: sha512-mPT+SD2TrlB6wvte1KkYOYUkubaTbd6pZ/6Kk3C9nxzrHmCZyhxOO7XGAeL7f+yLKZglzGtM9odUVvg/EhO+vQ==, + } + engines: { node: ">=10" } serve-placeholder@2.0.2: - resolution: {integrity: sha512-/TMG8SboeiQbZJWRlfTCqMs2DD3SZgWp0kDQePz9yUuCnDfDh/92gf7/PxGhzXTKBIPASIHxFcZndoNbp6QOLQ==} + resolution: + { + integrity: sha512-/TMG8SboeiQbZJWRlfTCqMs2DD3SZgWp0kDQePz9yUuCnDfDh/92gf7/PxGhzXTKBIPASIHxFcZndoNbp6QOLQ==, + } serve-static@2.2.1: - resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} - engines: {node: '>= 18'} + resolution: + { + integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==, + } + engines: { node: ">= 18" } setprototypeof@1.1.0: - resolution: {integrity: sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==} + resolution: + { + integrity: sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==, + } setprototypeof@1.2.0: - resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + resolution: + { + integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==, + } shebang-command@2.0.0: - resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==, + } + engines: { node: ">=8" } shebang-regex@3.0.0: - resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==, + } + engines: { node: ">=8" } shell-quote@1.10.0: - resolution: {integrity: sha512-w1aiOKwKuRgtwAReIIj89puqg+I7GvX4IbLrvmhXbzQsj1+Zwi4VO3+fa6ZF91TWSjIxoEkKnMeHcLEODK5ZXA==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-w1aiOKwKuRgtwAReIIj89puqg+I7GvX4IbLrvmhXbzQsj1+Zwi4VO3+fa6ZF91TWSjIxoEkKnMeHcLEODK5ZXA==, + } + engines: { node: ">= 0.4" } signal-exit@3.0.7: - resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + resolution: + { + integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==, + } signal-exit@4.1.0: - resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} - engines: {node: '>=14'} + resolution: + { + integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==, + } + engines: { node: ">=14" } simple-git@3.36.0: - resolution: {integrity: sha512-cGQjLjK8bxJw4QuYT7gxHw3/IouVESbhahSsHrX97MzCL1gu2u7oy38W6L2ZIGECEfIBG4BabsWDPjBxJENv9Q==} + resolution: + { + integrity: sha512-cGQjLjK8bxJw4QuYT7gxHw3/IouVESbhahSsHrX97MzCL1gu2u7oy38W6L2ZIGECEfIBG4BabsWDPjBxJENv9Q==, + } sirv@3.0.2: - resolution: {integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==, + } + engines: { node: ">=18" } sisteransi@1.0.5: - resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + resolution: + { + integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==, + } slash@5.1.0: - resolution: {integrity: sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==} - engines: {node: '>=14.16'} + resolution: + { + integrity: sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==, + } + engines: { node: ">=14.16" } smob@1.6.2: - resolution: {integrity: sha512-RQsvleCbF8cVHEv+xuDGaA4pOizFqJ0GgjtMSRo6oP8pnN7WsigHgVGey6aILRBKv4W2YOMHLqbKdnB6hpB9fw==} - engines: {node: '>=20.0.0'} + resolution: + { + integrity: sha512-RQsvleCbF8cVHEv+xuDGaA4pOizFqJ0GgjtMSRo6oP8pnN7WsigHgVGey6aILRBKv4W2YOMHLqbKdnB6hpB9fw==, + } + engines: { node: ">=20.0.0" } sortablejs@1.14.0: - resolution: {integrity: sha512-pBXvQCs5/33fdN1/39pPL0NZF20LeRbLQ5jtnheIPN9JQAaufGjKdWduZn4U7wCtVuzKhmRkI0DFYHYRbB2H1w==} + resolution: + { + integrity: sha512-pBXvQCs5/33fdN1/39pPL0NZF20LeRbLQ5jtnheIPN9JQAaufGjKdWduZn4U7wCtVuzKhmRkI0DFYHYRbB2H1w==, + } source-map-js@1.2.1: - resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} - engines: {node: '>=0.10.0'} + resolution: + { + integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==, + } + engines: { node: ">=0.10.0" } source-map-support@0.5.21: - resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + resolution: + { + integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==, + } source-map@0.6.1: - resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} - engines: {node: '>=0.10.0'} + resolution: + { + integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==, + } + engines: { node: ">=0.10.0" } source-map@0.7.6: - resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} - engines: {node: '>= 12'} + resolution: + { + integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==, + } + engines: { node: ">= 12" } srvx@0.11.22: - resolution: {integrity: sha512-LqZxxBDMKuMAZzFzJnDCkFOrs9MZQZr0LvHiO/SuSZVdQaXD7xQ5UWTUxheJrQPve1qk9MG2B/yttUvJxw8egQ==} - engines: {node: '>=20.16.0'} + resolution: + { + integrity: sha512-LqZxxBDMKuMAZzFzJnDCkFOrs9MZQZr0LvHiO/SuSZVdQaXD7xQ5UWTUxheJrQPve1qk9MG2B/yttUvJxw8egQ==, + } + engines: { node: ">=20.16.0" } hasBin: true standard-as-callback@2.1.0: - resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==} + resolution: + { + integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==, + } statuses@1.5.0: - resolution: {integrity: sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==} - engines: {node: '>= 0.6'} + resolution: + { + integrity: sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==, + } + engines: { node: ">= 0.6" } statuses@2.0.2: - resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} - engines: {node: '>= 0.8'} + resolution: + { + integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==, + } + engines: { node: ">= 0.8" } std-env@4.2.0: - resolution: {integrity: sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==} + resolution: + { + integrity: sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==, + } streamx@2.28.0: - resolution: {integrity: sha512-1Yowhzjf0ivGMrTIkY9hav5TxobO9qIVqUE41fiCGMGgc3CLlf4MY+9AHmZqBWgDTue0fY9zWjYFVyf6Diuobw==} + resolution: + { + integrity: sha512-1Yowhzjf0ivGMrTIkY9hav5TxobO9qIVqUE41fiCGMGgc3CLlf4MY+9AHmZqBWgDTue0fY9zWjYFVyf6Diuobw==, + } string-width@4.2.3: - resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==, + } + engines: { node: ">=8" } string-width@5.1.2: - resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==, + } + engines: { node: ">=12" } string-width@7.2.0: - resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==, + } + engines: { node: ">=18" } string-width@8.2.2: - resolution: {integrity: sha512-GaPUh5gfdrYzqeVNZvUfT23vYYxXzKYidUcnMtJg/3rxRV63EFZy3k6xfKlmfeJD0176lnUV/Usr3XcwSvFzpg==} - engines: {node: '>=20'} + resolution: + { + integrity: sha512-GaPUh5gfdrYzqeVNZvUfT23vYYxXzKYidUcnMtJg/3rxRV63EFZy3k6xfKlmfeJD0176lnUV/Usr3XcwSvFzpg==, + } + engines: { node: ">=20" } string_decoder@1.1.1: - resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + resolution: + { + integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==, + } string_decoder@1.3.0: - resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + resolution: + { + integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==, + } strip-ansi@6.0.1: - resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==, + } + engines: { node: ">=8" } strip-ansi@7.2.0: - resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==, + } + engines: { node: ">=12" } strip-final-newline@3.0.0: - resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==, + } + engines: { node: ">=12" } strip-literal@4.0.0: - resolution: {integrity: sha512-PaqAvfUZKBwc/SLmNZtHmzK+v19Z4O4eS3cKPeGvbIv/U3pnyEq4Tuw3/4v/FwfM8VQaEawsyCcOQ0P+kpwWWw==} + resolution: + { + integrity: sha512-PaqAvfUZKBwc/SLmNZtHmzK+v19Z4O4eS3cKPeGvbIv/U3pnyEq4Tuw3/4v/FwfM8VQaEawsyCcOQ0P+kpwWWw==, + } structured-clone-es@2.0.1: - resolution: {integrity: sha512-10ZL5r77LhknxlP1FBiCW+VdnuWOEFLdSS2SKtjyEV+L4qP1hUEIMIZW94LC3jKxRmT/Dj7KkD1mqh/IY6lhKQ==} + resolution: + { + integrity: sha512-10ZL5r77LhknxlP1FBiCW+VdnuWOEFLdSS2SKtjyEV+L4qP1hUEIMIZW94LC3jKxRmT/Dj7KkD1mqh/IY6lhKQ==, + } stylehacks@8.0.2: - resolution: {integrity: sha512-3d5vjQODiMc4VwED1w24fAYSfeAjdIRLy88cJQ9NAYkZr/6ozCVbusda5N+0kXsfGWW3pizUgYK2TOZxiU0cSQ==} - engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} + resolution: + { + integrity: sha512-3d5vjQODiMc4VwED1w24fAYSfeAjdIRLy88cJQ9NAYkZr/6ozCVbusda5N+0kXsfGWW3pizUgYK2TOZxiU0cSQ==, + } + engines: { node: ^22.11.0 || ^24.11.0 || >=26.0 } peerDependencies: postcss: ^8.5.25 sucrase@3.35.0: - resolution: {integrity: sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==} - engines: {node: '>=16 || 14 >=14.17'} + resolution: + { + integrity: sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==, + } + engines: { node: ">=16 || 14 >=14.17" } hasBin: true supports-color@10.2.2: - resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==, + } + engines: { node: ">=18" } supports-color@7.2.0: - resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==, + } + engines: { node: ">=8" } supports-color@8.1.1: - resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} - engines: {node: '>=10'} + resolution: + { + integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==, + } + engines: { node: ">=10" } supports-preserve-symlinks-flag@1.0.0: - resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==, + } + engines: { node: ">= 0.4" } svgo@4.0.2: - resolution: {integrity: sha512-ekx94z1rRc5LDi6oSUaeRnYhd0UOJxdtQCL2rF8xpWxD3TPAsISWOrxezqGovqS38GRZOdpDfvQe3ts6F7nsng==} - engines: {node: '>=16'} + resolution: + { + integrity: sha512-ekx94z1rRc5LDi6oSUaeRnYhd0UOJxdtQCL2rF8xpWxD3TPAsISWOrxezqGovqS38GRZOdpDfvQe3ts6F7nsng==, + } + engines: { node: ">=16" } hasBin: true sync-child-process@1.0.2: - resolution: {integrity: sha512-8lD+t2KrrScJ/7KXCSyfhT3/hRq78rC0wBFqNJXv3mZyn6hW2ypM05JmlSvtqRbeq6jqA94oHbxAr2vYsJ8vDA==} - engines: {node: '>=16.0.0'} + resolution: + { + integrity: sha512-8lD+t2KrrScJ/7KXCSyfhT3/hRq78rC0wBFqNJXv3mZyn6hW2ypM05JmlSvtqRbeq6jqA94oHbxAr2vYsJ8vDA==, + } + engines: { node: ">=16.0.0" } sync-message-port@1.1.3: - resolution: {integrity: sha512-GTt8rSKje5FilG+wEdfCkOcLL7LWqpMlr2c3LRuKt/YXxcJ52aGSbGBAdI4L3aaqfrBt6y711El53ItyH1NWzg==} - engines: {node: '>=16.0.0'} + resolution: + { + integrity: sha512-GTt8rSKje5FilG+wEdfCkOcLL7LWqpMlr2c3LRuKt/YXxcJ52aGSbGBAdI4L3aaqfrBt6y711El53ItyH1NWzg==, + } + engines: { node: ">=16.0.0" } tagged-tag@1.0.0: - resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==} - engines: {node: '>=20'} + resolution: + { + integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==, + } + engines: { node: ">=20" } tailwind-config-viewer@2.0.4: - resolution: {integrity: sha512-icvcmdMmt9dphvas8wL40qttrHwAnW3QEN4ExJ2zICjwRsPj7gowd1cOceaWG3IfTuM/cTNGQcx+bsjMtmV+cw==} - engines: {node: '>=13'} + resolution: + { + integrity: sha512-icvcmdMmt9dphvas8wL40qttrHwAnW3QEN4ExJ2zICjwRsPj7gowd1cOceaWG3IfTuM/cTNGQcx+bsjMtmV+cw==, + } + engines: { node: ">=13" } hasBin: true peerDependencies: tailwindcss: 1 || 2 || 2.0.1-compat || 3 tailwindcss@3.4.18: - resolution: {integrity: sha512-6A2rnmW5xZMdw11LYjhcI5846rt9pbLSabY5XPxo+XWdxwZaFEn47Go4NzFiHu9sNNmr/kXivP1vStfvMaK1GQ==} - engines: {node: '>=14.0.0'} + resolution: + { + integrity: sha512-6A2rnmW5xZMdw11LYjhcI5846rt9pbLSabY5XPxo+XWdxwZaFEn47Go4NzFiHu9sNNmr/kXivP1vStfvMaK1GQ==, + } + engines: { node: ">=14.0.0" } hasBin: true tar-stream@3.2.0: - resolution: {integrity: sha512-ojzvCvVaNp6aOTFmG7jaRD0meowIAuPc3cMMhSgKiVWws1GyHbGd/xvnyuRKcKlMpt3qvxx6r0hreCNITP9hIg==} + resolution: + { + integrity: sha512-ojzvCvVaNp6aOTFmG7jaRD0meowIAuPc3cMMhSgKiVWws1GyHbGd/xvnyuRKcKlMpt3qvxx6r0hreCNITP9hIg==, + } tar@7.5.22: - resolution: {integrity: sha512-MFO/QzvtAOmJbkhOaCTvbGcFN9L9b+JunIsDwaKljSOdcLMea3NJ1k9Usz/rjdfSXTq4dfzfeS7W4p4YOAAHeA==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-MFO/QzvtAOmJbkhOaCTvbGcFN9L9b+JunIsDwaKljSOdcLMea3NJ1k9Usz/rjdfSXTq4dfzfeS7W4p4YOAAHeA==, + } + engines: { node: ">=18" } teex@1.0.1: - resolution: {integrity: sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==} + resolution: + { + integrity: sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==, + } terser@5.49.2: - resolution: {integrity: sha512-rGbJiKeQ4WDe3EXlDAIaQcwftVfv2Q8o1awFNfvXolJYKkb1AuZY1RTOmqx4LJXZENbWZA7eIsYGHuEzHsi1nQ==} - engines: {node: '>=10'} + resolution: + { + integrity: sha512-rGbJiKeQ4WDe3EXlDAIaQcwftVfv2Q8o1awFNfvXolJYKkb1AuZY1RTOmqx4LJXZENbWZA7eIsYGHuEzHsi1nQ==, + } + engines: { node: ">=10" } hasBin: true text-decoder@1.2.7: - resolution: {integrity: sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==} + resolution: + { + integrity: sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==, + } thenify-all@1.6.0: - resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} - engines: {node: '>=0.8'} + resolution: + { + integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==, + } + engines: { node: ">=0.8" } thenify@3.3.1: - resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + resolution: + { + integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==, + } tiny-invariant@1.3.3: - resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} + resolution: + { + integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==, + } tinyclip@0.1.15: - resolution: {integrity: sha512-uo33abH+Ays0xYaDysoBt494Hb3hsEczMpcC0MwFl773pazORx4fmvKhclhR1wonUbB6vvpRsvVMwnhfqeMc+A==} - engines: {node: ^16.14.0 || >= 17.3.0} + resolution: + { + integrity: sha512-uo33abH+Ays0xYaDysoBt494Hb3hsEczMpcC0MwFl773pazORx4fmvKhclhR1wonUbB6vvpRsvVMwnhfqeMc+A==, + } + engines: { node: ^16.14.0 || >= 17.3.0 } tinyexec@1.3.0: - resolution: {integrity: sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==, + } + engines: { node: ">=18" } tinyglobby@0.2.17: - resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} - engines: {node: '>=12.0.0'} + resolution: + { + integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==, + } + engines: { node: ">=12.0.0" } to-regex-range@5.0.1: - resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} - engines: {node: '>=8.0'} + resolution: + { + integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==, + } + engines: { node: ">=8.0" } toidentifier@1.0.1: - resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} - engines: {node: '>=0.6'} + resolution: + { + integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==, + } + engines: { node: ">=0.6" } totalist@3.0.1: - resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} - engines: {node: '>=6'} + resolution: + { + integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==, + } + engines: { node: ">=6" } tr46@0.0.3: - resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + resolution: + { + integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==, + } ts-interface-checker@0.1.13: - resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + resolution: + { + integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==, + } tslib@2.8.1: - resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + resolution: + { + integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==, + } tsscmp@1.0.6: - resolution: {integrity: sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==} - engines: {node: '>=0.6.x'} + resolution: + { + integrity: sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==, + } + engines: { node: ">=0.6.x" } type-fest@5.8.0: - resolution: {integrity: sha512-YGYEVz3Fm5iy/AybuA0oyNFq7H4CgQNfRp/qfe8nurE1kuCeNm3/vfm9X4Mtl+qLyaKJUh5xrFZwogr41SMjYA==} - engines: {node: '>=20'} + resolution: + { + integrity: sha512-YGYEVz3Fm5iy/AybuA0oyNFq7H4CgQNfRp/qfe8nurE1kuCeNm3/vfm9X4Mtl+qLyaKJUh5xrFZwogr41SMjYA==, + } + engines: { node: ">=20" } type-is@1.6.18: - resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} - engines: {node: '>= 0.6'} + resolution: + { + integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==, + } + engines: { node: ">= 0.6" } typescript@5.9.3: - resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} - engines: {node: '>=14.17'} + resolution: + { + integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==, + } + engines: { node: ">=14.17" } hasBin: true uc.micro@2.1.0: - resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==} + resolution: + { + integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==, + } ufo@1.6.4: - resolution: {integrity: sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==} + resolution: + { + integrity: sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==, + } ultrahtml@1.7.0: - resolution: {integrity: sha512-2xRd0VHoAQE4M+vF/DvFFB7pUV0ZxTW1TLi7lHQWnF/Sb5TPeEUV/l+hxcNnGO00ZXGnR0voCMmYRKQf+rvJ2g==} + resolution: + { + integrity: sha512-2xRd0VHoAQE4M+vF/DvFFB7pUV0ZxTW1TLi7lHQWnF/Sb5TPeEUV/l+hxcNnGO00ZXGnR0voCMmYRKQf+rvJ2g==, + } uncrypto@0.1.3: - resolution: {integrity: sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==} + resolution: + { + integrity: sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==, + } unctx@2.5.0: - resolution: {integrity: sha512-p+Rz9x0R7X+CYDkT+Xg8/GhpcShTlU8n+cf9OtOEf7zEQsNcCZO1dPKNRDqvUTaq+P32PMMkxWHwfrxkqfqAYg==} + resolution: + { + integrity: sha512-p+Rz9x0R7X+CYDkT+Xg8/GhpcShTlU8n+cf9OtOEf7zEQsNcCZO1dPKNRDqvUTaq+P32PMMkxWHwfrxkqfqAYg==, + } unctx@3.0.0: - resolution: {integrity: sha512-DoXdZVeyi2jyEsn86i8MO5RTItm1kffUkH9/+DQORn3Q688AMOy2551CIl6AdGL2UpwD675wtbNOl75wIQN/uA==} + resolution: + { + integrity: sha512-DoXdZVeyi2jyEsn86i8MO5RTItm1kffUkH9/+DQORn3Q688AMOy2551CIl6AdGL2UpwD675wtbNOl75wIQN/uA==, + } peerDependencies: - magic-string: '>=0.30.21' - oxc-parser: '>=0.140.0' + magic-string: ">=0.30.21" + oxc-parser: ">=0.140.0" rolldown: ^1.1.5 unplugin: ^3.3.0 peerDependenciesMeta: @@ -3794,36 +6276,57 @@ packages: optional: true undici-types@7.18.2: - resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} + resolution: + { + integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==, + } undici@8.10.0: - resolution: {integrity: sha512-HvltHd7avK13QIw/oLe4qoOLyoVSoafqJ2jYOrtMRBkbYT31eiBQ8O0ehRKZiEZCMEyLFQNIADpgCWC5fALvYQ==} - engines: {node: '>=22.19.0'} + resolution: + { + integrity: sha512-HvltHd7avK13QIw/oLe4qoOLyoVSoafqJ2jYOrtMRBkbYT31eiBQ8O0ehRKZiEZCMEyLFQNIADpgCWC5fALvYQ==, + } + engines: { node: ">=22.19.0" } unenv@2.0.0-rc.24: - resolution: {integrity: sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==} + resolution: + { + integrity: sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==, + } unhead@3.3.1: - resolution: {integrity: sha512-eqHlbLyuvIXw898WopQmosTml4PdYW5ZhXDom/sf7ysAqQB9uvYrw2/dNvbrmkJTufSkmNmgGCjoQcvoT2mS/A==} + resolution: + { + integrity: sha512-eqHlbLyuvIXw898WopQmosTml4PdYW5ZhXDom/sf7ysAqQB9uvYrw2/dNvbrmkJTufSkmNmgGCjoQcvoT2mS/A==, + } peerDependencies: - vite: '>=6.4.2' + vite: ">=6.4.2" peerDependenciesMeta: vite: optional: true unicorn-magic@0.3.0: - resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==, + } + engines: { node: ">=18" } unicorn-magic@0.4.0: - resolution: {integrity: sha512-wH590V9VNgYH9g3lH9wWjTrUoKsjLF6sGLjhR4sH1LWpLmCOH0Zf7PukhDA8BiS7KHe4oPNkcTHqYkj7SOGUOw==} - engines: {node: '>=20'} + resolution: + { + integrity: sha512-wH590V9VNgYH9g3lH9wWjTrUoKsjLF6sGLjhR4sH1LWpLmCOH0Zf7PukhDA8BiS7KHe4oPNkcTHqYkj7SOGUOw==, + } + engines: { node: ">=20" } unimport@6.4.0: - resolution: {integrity: sha512-JJOOuNMFq8b4ZPBKwQUxEcba4MplskDzYI1Lvrf8rJfWphZTWvPNXWa493qsPngHUmub89w6C7j+SeLWTE/UIQ==} - engines: {node: '>=18.12.0'} + resolution: + { + integrity: sha512-JJOOuNMFq8b4ZPBKwQUxEcba4MplskDzYI1Lvrf8rJfWphZTWvPNXWa493qsPngHUmub89w6C7j+SeLWTE/UIQ==, + } + engines: { node: ">=18.12.0" } peerDependencies: - oxc-parser: '*' + oxc-parser: "*" rolldown: ^1.0.0 peerDependenciesMeta: oxc-parser: @@ -3832,34 +6335,46 @@ packages: optional: true universalify@2.0.1: - resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} - engines: {node: '>= 10.0.0'} + resolution: + { + integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==, + } + engines: { node: ">= 10.0.0" } unplugin-utils@0.3.2: - resolution: {integrity: sha512-xVToRh2CTmLk2HnEG7ac4rl1MJTT3RFkpS8B++/SnB0kXvuaavD+n3m/vrzyWQOdJNSZQACnbz01pnppbwV5BA==} - engines: {node: '>=20.19.0'} + resolution: + { + integrity: sha512-xVToRh2CTmLk2HnEG7ac4rl1MJTT3RFkpS8B++/SnB0kXvuaavD+n3m/vrzyWQOdJNSZQACnbz01pnppbwV5BA==, + } + engines: { node: ">=20.19.0" } unplugin@2.3.11: - resolution: {integrity: sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==} - engines: {node: '>=18.12.0'} + resolution: + { + integrity: sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==, + } + engines: { node: ">=18.12.0" } unplugin@3.3.0: - resolution: {integrity: sha512-qa66K+crbfyE6JK10GjvbJeRrOsuC/JpbnHctfyp/i4oBTxWOzJfRZyDiOk1PtErMFRu8JhsU/wPvOdBNWe5Rg==} - engines: {node: ^20.19.0 || >=22.12.0} + resolution: + { + integrity: sha512-qa66K+crbfyE6JK10GjvbJeRrOsuC/JpbnHctfyp/i4oBTxWOzJfRZyDiOk1PtErMFRu8JhsU/wPvOdBNWe5Rg==, + } + engines: { node: ^20.19.0 || >=22.12.0 } peerDependencies: - '@farmfe/core': '*' - '@rspack/core': '*' - bun-types-no-globals: '*' - esbuild: '*' - rolldown: '*' - rollup: '*' - unloader: '*' - vite: '*' - webpack: '*' + "@farmfe/core": "*" + "@rspack/core": "*" + bun-types-no-globals: "*" + esbuild: "*" + rolldown: "*" + rollup: "*" + unloader: "*" + vite: "*" + webpack: "*" peerDependenciesMeta: - '@farmfe/core': + "@farmfe/core": optional: true - '@rspack/core': + "@rspack/core": optional: true bun-types-no-globals: optional: true @@ -3877,58 +6392,64 @@ packages: optional: true unrouting@0.2.2: - resolution: {integrity: sha512-EoCab68s1o9AWFq9fjKsMEsmjKrPO11SAsvopikks2eEm/KLXgZMCof4cgpVgdy0Vz71OFBJw6DoDqu1oOSFGw==} + resolution: + { + integrity: sha512-EoCab68s1o9AWFq9fjKsMEsmjKrPO11SAsvopikks2eEm/KLXgZMCof4cgpVgdy0Vz71OFBJw6DoDqu1oOSFGw==, + } unstorage@1.17.5: - resolution: {integrity: sha512-0i3iqvRfx29hkNntHyQvJTpf5W9dQ9ZadSoRU8+xVlhVtT7jAX57fazYO9EHvcRCfBCyi5YRya7XCDOsbTgkPg==} + resolution: + { + integrity: sha512-0i3iqvRfx29hkNntHyQvJTpf5W9dQ9ZadSoRU8+xVlhVtT7jAX57fazYO9EHvcRCfBCyi5YRya7XCDOsbTgkPg==, + } peerDependencies: - '@azure/app-configuration': ^1.8.0 - '@azure/cosmos': ^4.2.0 - '@azure/data-tables': ^13.3.0 - '@azure/identity': ^4.6.0 - '@azure/keyvault-secrets': ^4.9.0 - '@azure/storage-blob': ^12.26.0 - '@capacitor/preferences': ^6 || ^7 || ^8 - '@deno/kv': '>=0.9.0' - '@netlify/blobs': ^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0 - '@planetscale/database': ^1.19.0 - '@upstash/redis': ^1.34.3 - '@vercel/blob': '>=0.27.1' - '@vercel/functions': ^2.2.12 || ^3.0.0 - '@vercel/kv': ^1 || ^2 || ^3 + "@azure/app-configuration": ^1.8.0 + "@azure/cosmos": ^4.2.0 + "@azure/data-tables": ^13.3.0 + "@azure/identity": ^4.6.0 + "@azure/keyvault-secrets": ^4.9.0 + "@azure/storage-blob": ^12.26.0 + "@capacitor/preferences": ^6 || ^7 || ^8 + "@deno/kv": ">=0.9.0" + "@netlify/blobs": ^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0 + "@planetscale/database": ^1.19.0 + "@upstash/redis": ^1.34.3 + "@vercel/blob": ">=0.27.1" + "@vercel/functions": ^2.2.12 || ^3.0.0 + "@vercel/kv": ^1 || ^2 || ^3 aws4fetch: ^1.0.20 - db0: '>=0.2.1' + db0: ">=0.2.1" idb-keyval: ^6.2.1 ioredis: ^5.4.2 uploadthing: ^7.4.4 peerDependenciesMeta: - '@azure/app-configuration': + "@azure/app-configuration": optional: true - '@azure/cosmos': + "@azure/cosmos": optional: true - '@azure/data-tables': + "@azure/data-tables": optional: true - '@azure/identity': + "@azure/identity": optional: true - '@azure/keyvault-secrets': + "@azure/keyvault-secrets": optional: true - '@azure/storage-blob': + "@azure/storage-blob": optional: true - '@capacitor/preferences': + "@capacitor/preferences": optional: true - '@deno/kv': + "@deno/kv": optional: true - '@netlify/blobs': + "@netlify/blobs": optional: true - '@planetscale/database': + "@planetscale/database": optional: true - '@upstash/redis': + "@upstash/redis": optional: true - '@vercel/blob': + "@vercel/blob": optional: true - '@vercel/functions': + "@vercel/functions": optional: true - '@vercel/kv': + "@vercel/kv": optional: true aws4fetch: optional: true @@ -3942,75 +6463,117 @@ packages: optional: true untun@0.2.2: - resolution: {integrity: sha512-+NnOJcSiEtYsVgJmXUzQbJeRAFXJC4yPJYuh6kF9B0Rm6zunXcs/3GZOTllyocSbUDIxD6Bj7e/4ATw7sph1Sw==} + resolution: + { + integrity: sha512-+NnOJcSiEtYsVgJmXUzQbJeRAFXJC4yPJYuh6kF9B0Rm6zunXcs/3GZOTllyocSbUDIxD6Bj7e/4ATw7sph1Sw==, + } hasBin: true untyped@2.0.0: - resolution: {integrity: sha512-nwNCjxJTjNuLCgFr42fEak5OcLuB3ecca+9ksPFNvtfYSLpjf+iJqSIaSnIile6ZPbKYxI5k2AfXqeopGudK/g==} + resolution: + { + integrity: sha512-nwNCjxJTjNuLCgFr42fEak5OcLuB3ecca+9ksPFNvtfYSLpjf+iJqSIaSnIile6ZPbKYxI5k2AfXqeopGudK/g==, + } hasBin: true unwasm@0.5.3: - resolution: {integrity: sha512-keBgTSfp3r6+s9ZcSma+0chwxQdmLbB5+dAD9vjtB21UTMYuKAxHXCU1K2CbCtnP09EaWeRvACnXk0EJtUx+hw==} + resolution: + { + integrity: sha512-keBgTSfp3r6+s9ZcSma+0chwxQdmLbB5+dAD9vjtB21UTMYuKAxHXCU1K2CbCtnP09EaWeRvACnXk0EJtUx+hw==, + } update-browserslist-db@1.2.3: - resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + resolution: + { + integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==, + } hasBin: true peerDependencies: - browserslist: '>= 4.21.0' + browserslist: ">= 4.21.0" update-browserslist-db@1.3.0: - resolution: {integrity: sha512-x/M6q3w4Ybp91CNaS4S69UnliqR3BzRpOT6LWbksjth0S/+jhfaPJsWjt/TewpT8j9eLIojUf5jr29WextHroA==} + resolution: + { + integrity: sha512-x/M6q3w4Ybp91CNaS4S69UnliqR3BzRpOT6LWbksjth0S/+jhfaPJsWjt/TewpT8j9eLIojUf5jr29WextHroA==, + } hasBin: true peerDependencies: - browserslist: '>= 4.21.0' + browserslist: ">= 4.21.0" uqr@0.1.3: - resolution: {integrity: sha512-0rjE8iEJe4YmT9TOhwsZtqCMRLc5DXZUI2UEYUUg63ikBkqqE5EYWaI0etFe/5KUcmcYwLih2RND1kq+hrUJXA==} + resolution: + { + integrity: sha512-0rjE8iEJe4YmT9TOhwsZtqCMRLc5DXZUI2UEYUUg63ikBkqqE5EYWaI0etFe/5KUcmcYwLih2RND1kq+hrUJXA==, + } util-deprecate@1.0.2: - resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + resolution: + { + integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==, + } varint@6.0.0: - resolution: {integrity: sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg==} + resolution: + { + integrity: sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg==, + } vary@1.1.2: - resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} - engines: {node: '>= 0.8'} + resolution: + { + integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==, + } + engines: { node: ">= 0.8" } verkit@0.2.0: - resolution: {integrity: sha512-6M8R2xSKplkQ+0mAm07IMh548GLLPUnRQZJCCe/W4rfk/SXW2bs8ZJSWa5kjdIdsx3hLG5oLWROJ4+N5hpX08g==} - engines: {node: '>=18.12.0'} + resolution: + { + integrity: sha512-6M8R2xSKplkQ+0mAm07IMh548GLLPUnRQZJCCe/W4rfk/SXW2bs8ZJSWa5kjdIdsx3hLG5oLWROJ4+N5hpX08g==, + } + engines: { node: ">=18.12.0" } vite-dev-rpc@2.0.0: - resolution: {integrity: sha512-yKwbTwdHKSD2k/aGqyWpPHepo45OQc8lH3/6IfT4ZqeKE26ooKvi4WIEKzqWav8v+9Is8u1k8q54hvOmqASazA==} + resolution: + { + integrity: sha512-yKwbTwdHKSD2k/aGqyWpPHepo45OQc8lH3/6IfT4ZqeKE26ooKvi4WIEKzqWav8v+9Is8u1k8q54hvOmqASazA==, + } peerDependencies: vite: ^2.9.0 || ^3.0.0-0 || ^4.0.0-0 || ^5.0.0-0 || ^6.0.1 || ^7.0.0-0 || ^8.0.0 vite-hot-client@2.2.0: - resolution: {integrity: sha512-76Zs9zrHbH7M7wqeyooGQKdX+yg0pQ0xuQ1PbFp4z5a0Lzn2e5IPFoCswnmqZ4GiwqB4Jo3WcDAMO9jARTJl8w==} + resolution: + { + integrity: sha512-76Zs9zrHbH7M7wqeyooGQKdX+yg0pQ0xuQ1PbFp4z5a0Lzn2e5IPFoCswnmqZ4GiwqB4Jo3WcDAMO9jARTJl8w==, + } peerDependencies: vite: ^2.6.0 || ^3.0.0 || ^4.0.0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0 || ^8.0.0 vite-node@6.0.0: - resolution: {integrity: sha512-oj4PVrT+pDh6GYf5wfUXkcZyekYS8kKPfLPXVl8qe324Ec6l4K2DUKNadRbZ3LQl0qGcDz+PyOo7ZAh00Y+JjQ==} - engines: {node: ^20.19.0 || >=22.12.0} + resolution: + { + integrity: sha512-oj4PVrT+pDh6GYf5wfUXkcZyekYS8kKPfLPXVl8qe324Ec6l4K2DUKNadRbZ3LQl0qGcDz+PyOo7ZAh00Y+JjQ==, + } + engines: { node: ^20.19.0 || >=22.12.0 } hasBin: true vite-plugin-checker@0.14.5: - resolution: {integrity: sha512-c9lQ92eisUO+F7Fd93aelojmiOS+NQpPgQ1XR2LTQHox1/laZf4yAoQj+L3RA9Vgh10e2nFd9b8r2LLyYZsbpA==} - engines: {node: '>=20.19.0'} + resolution: + { + integrity: sha512-c9lQ92eisUO+F7Fd93aelojmiOS+NQpPgQ1XR2LTQHox1/laZf4yAoQj+L3RA9Vgh10e2nFd9b8r2LLyYZsbpA==, + } + engines: { node: ">=20.19.0" } peerDependencies: - '@biomejs/biome': '>=2.4.12' - eslint: '>=9.39.4' + "@biomejs/biome": ">=2.4.12" + eslint: ">=9.39.4" meow: ^13.2.0 || ^14.0.0 optionator: ^0.9.4 - oxlint: '>=1' - stylelint: '>=16.26.1' - typescript: '*' - vite: '>=5.4.21' + oxlint: ">=1" + stylelint: ">=16.26.1" + typescript: "*" + vite: ">=5.4.21" vue-tsc: ~2.2.10 || ^3.0.0 peerDependenciesMeta: - '@biomejs/biome': + "@biomejs/biome": optional: true eslint: optional: true @@ -4028,42 +6591,51 @@ packages: optional: true vite-plugin-inspect@11.4.1: - resolution: {integrity: sha512-ShOFe2PURXGvRS5OrgmOLZOCwDTD7dEBVt0tMpFPKb9AsvqXKCRGM8QgKrUbRbJYFXScHvDPpGRd28rYidC0tA==} - engines: {node: '>=14'} + resolution: + { + integrity: sha512-ShOFe2PURXGvRS5OrgmOLZOCwDTD7dEBVt0tMpFPKb9AsvqXKCRGM8QgKrUbRbJYFXScHvDPpGRd28rYidC0tA==, + } + engines: { node: ">=14" } peerDependencies: - '@nuxt/kit': '*' + "@nuxt/kit": "*" vite: ^6.0.0 || ^7.0.0-0 || ^8.0.0-0 peerDependenciesMeta: - '@nuxt/kit': + "@nuxt/kit": optional: true vite-plugin-vue-tracer@1.4.0: - resolution: {integrity: sha512-0tQCjCqZWVSK6UeRW9S4ABbf47lKQ68zvrT2FNvZmiL+alDydCVyH/T3Jlfbdc3T3C2Iuyyl5aVsMbF8IQIoxA==} + resolution: + { + integrity: sha512-0tQCjCqZWVSK6UeRW9S4ABbf47lKQ68zvrT2FNvZmiL+alDydCVyH/T3Jlfbdc3T3C2Iuyyl5aVsMbF8IQIoxA==, + } peerDependencies: vite: ^6.0.0 || ^7.0.0 || ^8.0.0-0 vue: ^3.5.0 vite@8.2.1: - resolution: {integrity: sha512-EU/eS7BH3XROHh2YnBefjM6DBKA6ZeMZEYQbj7NLWg5wHYlhB8B/Mayd5XsgWq+NFYccDOTemRpdETWR6Ka/lw==} - engines: {node: ^20.19.0 || >=22.12.0} + resolution: + { + integrity: sha512-EU/eS7BH3XROHh2YnBefjM6DBKA6ZeMZEYQbj7NLWg5wHYlhB8B/Mayd5XsgWq+NFYccDOTemRpdETWR6Ka/lw==, + } + engines: { node: ^20.19.0 || >=22.12.0 } hasBin: true peerDependencies: - '@types/node': ^20.19.0 || >=22.12.0 - '@vitejs/devtools': ^0.4.0 + "@types/node": ^20.19.0 || >=22.12.0 + "@vitejs/devtools": ^0.4.0 esbuild: ^0.27.0 || ^0.28.0 - jiti: '>=1.21.0' + jiti: ">=1.21.0" less: ^4.0.0 sass: ^1.70.0 sass-embedded: ^1.70.0 - stylus: '>=0.54.8' + stylus: ">=0.54.8" sugarss: ^5.0.0 terser: ^5.16.0 tsx: ^4.8.1 yaml: ^2.4.2 peerDependenciesMeta: - '@types/node': + "@types/node": optional: true - '@vitejs/devtools': + "@vitejs/devtools": optional: true esbuild: optional: true @@ -4087,26 +6659,38 @@ packages: optional: true vscode-uri@3.1.0: - resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==} + resolution: + { + integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==, + } vue-bundle-renderer@2.3.1: - resolution: {integrity: sha512-7F4LNMopUw5RgYWo4zCmVUHCc6aQRC6dCKHUYkM/n+fux4AUGdL1x6m5A515WWyFysRRN7cx3hBzVqoisfRfzw==} + resolution: + { + integrity: sha512-7F4LNMopUw5RgYWo4zCmVUHCc6aQRC6dCKHUYkM/n+fux4AUGdL1x6m5A515WWyFysRRN7cx3hBzVqoisfRfzw==, + } vue-devtools-stub@0.1.0: - resolution: {integrity: sha512-RutnB7X8c5hjq39NceArgXg28WZtZpGc3+J16ljMiYnFhKvd8hITxSWQSQ5bvldxMDU6gG5mkxl1MTQLXckVSQ==} + resolution: + { + integrity: sha512-RutnB7X8c5hjq39NceArgXg28WZtZpGc3+J16ljMiYnFhKvd8hITxSWQSQ5bvldxMDU6gG5mkxl1MTQLXckVSQ==, + } vue-router@5.2.0: - resolution: {integrity: sha512-QAC5i0LEb1GLG0LXDQmHu8L7FX12j0KwU/JTKmLQUJMrn04gQdKP6Du+p0QwpHb3iy71vBlqnHQ8WAfOSAWhqw==} + resolution: + { + integrity: sha512-QAC5i0LEb1GLG0LXDQmHu8L7FX12j0KwU/JTKmLQUJMrn04gQdKP6Du+p0QwpHb3iy71vBlqnHQ8WAfOSAWhqw==, + } peerDependencies: - '@pinia/colada': '>=0.21.2' - '@vue/compiler-sfc': ^3.5.34 || ^4.0.0 + "@pinia/colada": ">=0.21.2" + "@vue/compiler-sfc": ^3.5.34 || ^4.0.0 pinia: ^3.0.4 || ^4.0.2 vite: ^7.3.0 || ^8.0.0 vue: ^3.5.34 || ^4.0.0 peerDependenciesMeta: - '@pinia/colada': + "@pinia/colada": optional: true - '@vue/compiler-sfc': + "@vue/compiler-sfc": optional: true pinia: optional: true @@ -4114,64 +6698,103 @@ packages: optional: true vue-tsc@2.2.12: - resolution: {integrity: sha512-P7OP77b2h/Pmk+lZdJ0YWs+5tJ6J2+uOQPo7tlBnY44QqQSPYvS0qVT4wqDJgwrZaLe47etJLLQRFia71GYITw==} + resolution: + { + integrity: sha512-P7OP77b2h/Pmk+lZdJ0YWs+5tJ6J2+uOQPo7tlBnY44QqQSPYvS0qVT4wqDJgwrZaLe47etJLLQRFia71GYITw==, + } hasBin: true peerDependencies: - typescript: '>=5.0.0' + typescript: ">=5.0.0" vue@3.5.41: - resolution: {integrity: sha512-2laE0p+aK+/AOPG/XL/WepOs/GlK755LJ1XECi9kDUrz1FKNw8rb2Xzlw9JS1rqEV55nb0ttsKxVlTCcd+R5cg==} + resolution: + { + integrity: sha512-2laE0p+aK+/AOPG/XL/WepOs/GlK755LJ1XECi9kDUrz1FKNw8rb2Xzlw9JS1rqEV55nb0ttsKxVlTCcd+R5cg==, + } peerDependencies: - typescript: '*' + typescript: "*" peerDependenciesMeta: typescript: optional: true vuedraggable@4.1.0: - resolution: {integrity: sha512-FU5HCWBmsf20GpP3eudURW3WdWTKIbEIQxh9/8GE806hydR9qZqRRxRE3RjqX7PkuLuMQG/A7n3cfj9rCEchww==} + resolution: + { + integrity: sha512-FU5HCWBmsf20GpP3eudURW3WdWTKIbEIQxh9/8GE806hydR9qZqRRxRE3RjqX7PkuLuMQG/A7n3cfj9rCEchww==, + } peerDependencies: vue: ^3.0.1 webidl-conversions@3.0.1: - resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + resolution: + { + integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==, + } webpack-virtual-modules@0.6.2: - resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} + resolution: + { + integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==, + } whatwg-url@5.0.0: - resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + resolution: + { + integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==, + } which@2.0.2: - resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} - engines: {node: '>= 8'} + resolution: + { + integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==, + } + engines: { node: ">= 8" } hasBin: true which@6.0.1: - resolution: {integrity: sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==} - engines: {node: ^20.17.0 || >=22.9.0} + resolution: + { + integrity: sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==, + } + engines: { node: ^20.17.0 || >=22.9.0 } hasBin: true wrap-ansi@7.0.0: - resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} - engines: {node: '>=10'} + resolution: + { + integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==, + } + engines: { node: ">=10" } wrap-ansi@8.1.0: - resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==, + } + engines: { node: ">=12" } wrap-ansi@9.0.2: - resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==, + } + engines: { node: ">=18" } wrappy@1.0.2: - resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + resolution: + { + integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==, + } ws@8.21.3: - resolution: {integrity: sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==} - engines: {node: '>=10.0.0'} + resolution: + { + integrity: sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==, + } + engines: { node: ">=10.0.0" } peerDependencies: bufferutil: ^4.0.1 - utf-8-validate: '>=5.0.2' + utf-8-validate: ">=5.0.2" peerDependenciesMeta: bufferutil: optional: true @@ -4179,79 +6802,117 @@ packages: optional: true wsl-utils@0.3.1: - resolution: {integrity: sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg==} - engines: {node: '>=20'} + resolution: + { + integrity: sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg==, + } + engines: { node: ">=20" } y18n@5.0.8: - resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} - engines: {node: '>=10'} + resolution: + { + integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==, + } + engines: { node: ">=10" } yallist@3.1.1: - resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + resolution: + { + integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==, + } yallist@5.0.0: - resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==, + } + engines: { node: ">=18" } yaml@2.9.0: - resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==} - engines: {node: '>= 14.6'} + resolution: + { + integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==, + } + engines: { node: ">= 14.6" } hasBin: true yargs-parser@21.1.1: - resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==, + } + engines: { node: ">=12" } yargs-parser@22.0.0: - resolution: {integrity: sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==} - engines: {node: ^20.19.0 || ^22.12.0 || >=23} + resolution: + { + integrity: sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==, + } + engines: { node: ^20.19.0 || ^22.12.0 || >=23 } yargs@17.7.2: - resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==, + } + engines: { node: ">=12" } yargs@18.1.0: - resolution: {integrity: sha512-2rAgRKu54VsHkqI0/tYkmluGXHD4KW7yZoycuqDQ15QOTnc2VVfy0nN/1eMhnQLO00A+dwtK20xuCnc1YGeUyg==} - engines: {node: ^20.19.0 || ^22.12.0 || >=23} + resolution: + { + integrity: sha512-2rAgRKu54VsHkqI0/tYkmluGXHD4KW7yZoycuqDQ15QOTnc2VVfy0nN/1eMhnQLO00A+dwtK20xuCnc1YGeUyg==, + } + engines: { node: ^20.19.0 || ^22.12.0 || >=23 } ylru@1.4.0: - resolution: {integrity: sha512-2OQsPNEmBCvXuFlIni/a+Rn+R2pHW9INm0BxXJ4hVDA8TirqMj+J/Rp9ItLatT/5pZqWwefVrTQcHpixsxnVlA==} - engines: {node: '>= 4.0.0'} + resolution: + { + integrity: sha512-2OQsPNEmBCvXuFlIni/a+Rn+R2pHW9INm0BxXJ4hVDA8TirqMj+J/Rp9ItLatT/5pZqWwefVrTQcHpixsxnVlA==, + } + engines: { node: ">= 4.0.0" } youch-core@0.3.3: - resolution: {integrity: sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==} + resolution: + { + integrity: sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==, + } youch@4.1.1: - resolution: {integrity: sha512-mxW3qiSnl+GRxXsaUMzv2Mbada1Y8CDltET9UxejDQe6DBYlSekghl5U5K0ReAikcHDi0G1vKZEmmo/NWAGKLA==} + resolution: + { + integrity: sha512-mxW3qiSnl+GRxXsaUMzv2Mbada1Y8CDltET9UxejDQe6DBYlSekghl5U5K0ReAikcHDi0G1vKZEmmo/NWAGKLA==, + } zip-stream@6.0.1: - resolution: {integrity: sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==} - engines: {node: '>= 14'} + resolution: + { + integrity: sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==, + } + engines: { node: ">= 14" } snapshots: + "@alloc/quick-lru@5.2.0": {} - '@alloc/quick-lru@5.2.0': {} - - '@babel/code-frame@7.29.7': + "@babel/code-frame@7.29.7": dependencies: - '@babel/helper-validator-identifier': 7.29.7 + "@babel/helper-validator-identifier": 7.29.7 js-tokens: 4.0.0 picocolors: 1.1.1 - '@babel/compat-data@7.29.7': {} + "@babel/compat-data@7.29.7": {} - '@babel/core@7.29.7': + "@babel/core@7.29.7": dependencies: - '@babel/code-frame': 7.29.7 - '@babel/generator': 7.29.8 - '@babel/helper-compilation-targets': 7.29.7 - '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7) - '@babel/helpers': 7.29.7 - '@babel/parser': 7.29.8 - '@babel/template': 7.29.7 - '@babel/traverse': 7.29.8 - '@babel/types': 7.29.8 - '@jridgewell/remapping': 2.3.5 + "@babel/code-frame": 7.29.7 + "@babel/generator": 7.29.8 + "@babel/helper-compilation-targets": 7.29.7 + "@babel/helper-module-transforms": 7.29.7(@babel/core@7.29.7) + "@babel/helpers": 7.29.7 + "@babel/parser": 7.29.8 + "@babel/template": 7.29.7 + "@babel/traverse": 7.29.8 + "@babel/types": 7.29.8 + "@jridgewell/remapping": 2.3.5 convert-source-map: 2.0.0 debug: 4.4.3 gensync: 1.0.0-beta.2 @@ -4260,212 +6921,212 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/generator@7.29.8': + "@babel/generator@7.29.8": dependencies: - '@babel/parser': 7.29.8 - '@babel/types': 7.29.8 - '@jridgewell/gen-mapping': 0.3.13 - '@jridgewell/trace-mapping': 0.3.31 + "@babel/parser": 7.29.8 + "@babel/types": 7.29.8 + "@jridgewell/gen-mapping": 0.3.13 + "@jridgewell/trace-mapping": 0.3.31 jsesc: 3.1.0 - '@babel/generator@8.0.0': + "@babel/generator@8.0.0": dependencies: - '@babel/parser': 8.0.4 - '@babel/types': 8.0.4 - '@jridgewell/gen-mapping': 0.3.13 - '@jridgewell/trace-mapping': 0.3.31 - '@types/jsesc': 2.5.1 + "@babel/parser": 8.0.4 + "@babel/types": 8.0.4 + "@jridgewell/gen-mapping": 0.3.13 + "@jridgewell/trace-mapping": 0.3.31 + "@types/jsesc": 2.5.1 jsesc: 3.1.0 - '@babel/helper-annotate-as-pure@7.29.7': + "@babel/helper-annotate-as-pure@7.29.7": dependencies: - '@babel/types': 7.29.8 + "@babel/types": 7.29.8 - '@babel/helper-compilation-targets@7.29.7': + "@babel/helper-compilation-targets@7.29.7": dependencies: - '@babel/compat-data': 7.29.7 - '@babel/helper-validator-option': 7.29.7 + "@babel/compat-data": 7.29.7 + "@babel/helper-validator-option": 7.29.7 browserslist: 4.28.7 lru-cache: 5.1.1 semver: 6.3.1 - '@babel/helper-create-class-features-plugin@7.29.7(@babel/core@7.29.7)': + "@babel/helper-create-class-features-plugin@7.29.7(@babel/core@7.29.7)": dependencies: - '@babel/core': 7.29.7 - '@babel/helper-annotate-as-pure': 7.29.7 - '@babel/helper-member-expression-to-functions': 7.29.7 - '@babel/helper-optimise-call-expression': 7.29.7 - '@babel/helper-replace-supers': 7.29.7(@babel/core@7.29.7) - '@babel/helper-skip-transparent-expression-wrappers': 7.29.7 - '@babel/traverse': 7.29.8 + "@babel/core": 7.29.7 + "@babel/helper-annotate-as-pure": 7.29.7 + "@babel/helper-member-expression-to-functions": 7.29.7 + "@babel/helper-optimise-call-expression": 7.29.7 + "@babel/helper-replace-supers": 7.29.7(@babel/core@7.29.7) + "@babel/helper-skip-transparent-expression-wrappers": 7.29.7 + "@babel/traverse": 7.29.8 semver: 6.3.1 transitivePeerDependencies: - supports-color - '@babel/helper-globals@7.29.7': {} + "@babel/helper-globals@7.29.7": {} - '@babel/helper-member-expression-to-functions@7.29.7': + "@babel/helper-member-expression-to-functions@7.29.7": dependencies: - '@babel/traverse': 7.29.8 - '@babel/types': 7.29.8 + "@babel/traverse": 7.29.8 + "@babel/types": 7.29.8 transitivePeerDependencies: - supports-color - '@babel/helper-module-imports@7.29.7': + "@babel/helper-module-imports@7.29.7": dependencies: - '@babel/traverse': 7.29.8 - '@babel/types': 7.29.8 + "@babel/traverse": 7.29.8 + "@babel/types": 7.29.8 transitivePeerDependencies: - supports-color - '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7)': + "@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7)": dependencies: - '@babel/core': 7.29.7 - '@babel/helper-module-imports': 7.29.7 - '@babel/helper-validator-identifier': 7.29.7 - '@babel/traverse': 7.29.8 + "@babel/core": 7.29.7 + "@babel/helper-module-imports": 7.29.7 + "@babel/helper-validator-identifier": 7.29.7 + "@babel/traverse": 7.29.8 transitivePeerDependencies: - supports-color - '@babel/helper-optimise-call-expression@7.29.7': + "@babel/helper-optimise-call-expression@7.29.7": dependencies: - '@babel/types': 7.29.8 + "@babel/types": 7.29.8 - '@babel/helper-plugin-utils@7.29.7': {} + "@babel/helper-plugin-utils@7.29.7": {} - '@babel/helper-replace-supers@7.29.7(@babel/core@7.29.7)': + "@babel/helper-replace-supers@7.29.7(@babel/core@7.29.7)": dependencies: - '@babel/core': 7.29.7 - '@babel/helper-member-expression-to-functions': 7.29.7 - '@babel/helper-optimise-call-expression': 7.29.7 - '@babel/traverse': 7.29.8 + "@babel/core": 7.29.7 + "@babel/helper-member-expression-to-functions": 7.29.7 + "@babel/helper-optimise-call-expression": 7.29.7 + "@babel/traverse": 7.29.8 transitivePeerDependencies: - supports-color - '@babel/helper-skip-transparent-expression-wrappers@7.29.7': + "@babel/helper-skip-transparent-expression-wrappers@7.29.7": dependencies: - '@babel/traverse': 7.29.8 - '@babel/types': 7.29.8 + "@babel/traverse": 7.29.8 + "@babel/types": 7.29.8 transitivePeerDependencies: - supports-color - '@babel/helper-string-parser@7.29.7': {} + "@babel/helper-string-parser@7.29.7": {} - '@babel/helper-string-parser@8.0.0': {} + "@babel/helper-string-parser@8.0.0": {} - '@babel/helper-validator-identifier@7.29.7': {} + "@babel/helper-validator-identifier@7.29.7": {} - '@babel/helper-validator-identifier@8.0.4': {} + "@babel/helper-validator-identifier@8.0.4": {} - '@babel/helper-validator-option@7.29.7': {} + "@babel/helper-validator-option@7.29.7": {} - '@babel/helpers@7.29.7': + "@babel/helpers@7.29.7": dependencies: - '@babel/template': 7.29.7 - '@babel/types': 7.29.8 + "@babel/template": 7.29.7 + "@babel/types": 7.29.8 - '@babel/parser@7.29.7': + "@babel/parser@7.29.7": dependencies: - '@babel/types': 7.29.7 + "@babel/types": 7.29.7 - '@babel/parser@7.29.8': + "@babel/parser@7.29.8": dependencies: - '@babel/types': 7.29.8 + "@babel/types": 7.29.8 - '@babel/parser@8.0.4': + "@babel/parser@8.0.4": dependencies: - '@babel/types': 8.0.4 + "@babel/types": 8.0.4 - '@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7)': + "@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7)": dependencies: - '@babel/core': 7.29.7 - '@babel/helper-plugin-utils': 7.29.7 + "@babel/core": 7.29.7 + "@babel/helper-plugin-utils": 7.29.7 - '@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7)': + "@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7)": dependencies: - '@babel/core': 7.29.7 - '@babel/helper-plugin-utils': 7.29.7 + "@babel/core": 7.29.7 + "@babel/helper-plugin-utils": 7.29.7 - '@babel/plugin-transform-typescript@7.29.7(@babel/core@7.29.7)': + "@babel/plugin-transform-typescript@7.29.7(@babel/core@7.29.7)": dependencies: - '@babel/core': 7.29.7 - '@babel/helper-annotate-as-pure': 7.29.7 - '@babel/helper-create-class-features-plugin': 7.29.7(@babel/core@7.29.7) - '@babel/helper-plugin-utils': 7.29.7 - '@babel/helper-skip-transparent-expression-wrappers': 7.29.7 - '@babel/plugin-syntax-typescript': 7.29.7(@babel/core@7.29.7) + "@babel/core": 7.29.7 + "@babel/helper-annotate-as-pure": 7.29.7 + "@babel/helper-create-class-features-plugin": 7.29.7(@babel/core@7.29.7) + "@babel/helper-plugin-utils": 7.29.7 + "@babel/helper-skip-transparent-expression-wrappers": 7.29.7 + "@babel/plugin-syntax-typescript": 7.29.7(@babel/core@7.29.7) transitivePeerDependencies: - supports-color - '@babel/template@7.29.7': + "@babel/template@7.29.7": dependencies: - '@babel/code-frame': 7.29.7 - '@babel/parser': 7.29.8 - '@babel/types': 7.29.8 + "@babel/code-frame": 7.29.7 + "@babel/parser": 7.29.8 + "@babel/types": 7.29.8 - '@babel/traverse@7.29.8': + "@babel/traverse@7.29.8": dependencies: - '@babel/code-frame': 7.29.7 - '@babel/generator': 7.29.8 - '@babel/helper-globals': 7.29.7 - '@babel/parser': 7.29.8 - '@babel/template': 7.29.7 - '@babel/types': 7.29.8 + "@babel/code-frame": 7.29.7 + "@babel/generator": 7.29.8 + "@babel/helper-globals": 7.29.7 + "@babel/parser": 7.29.8 + "@babel/template": 7.29.7 + "@babel/types": 7.29.8 debug: 4.4.3 transitivePeerDependencies: - supports-color - '@babel/types@7.29.7': + "@babel/types@7.29.7": dependencies: - '@babel/helper-string-parser': 7.29.7 - '@babel/helper-validator-identifier': 7.29.7 + "@babel/helper-string-parser": 7.29.7 + "@babel/helper-validator-identifier": 7.29.7 - '@babel/types@7.29.8': + "@babel/types@7.29.8": dependencies: - '@babel/helper-string-parser': 7.29.7 - '@babel/helper-validator-identifier': 7.29.7 + "@babel/helper-string-parser": 7.29.7 + "@babel/helper-validator-identifier": 7.29.7 - '@babel/types@8.0.4': + "@babel/types@8.0.4": dependencies: - '@babel/helper-string-parser': 8.0.0 - '@babel/helper-validator-identifier': 8.0.4 + "@babel/helper-string-parser": 8.0.0 + "@babel/helper-validator-identifier": 8.0.4 - '@bomb.sh/tab@0.0.19(cac@6.7.14)(citty@0.2.2)': + "@bomb.sh/tab@0.0.19(cac@6.7.14)(citty@0.2.2)": optionalDependencies: cac: 6.7.14 citty: 0.2.2 - '@bufbuild/protobuf@2.10.1': {} + "@bufbuild/protobuf@2.10.1": {} - '@clack/core@1.4.3': + "@clack/core@1.4.3": dependencies: fast-wrap-ansi: 0.2.2 sisteransi: 1.0.5 - '@clack/prompts@1.7.0': + "@clack/prompts@1.7.0": dependencies: - '@clack/core': 1.4.3 + "@clack/core": 1.4.3 fast-string-width: 3.0.2 fast-wrap-ansi: 0.2.2 sisteransi: 1.0.5 - '@cloudflare/kv-asset-handler@0.4.2': {} + "@cloudflare/kv-asset-handler@0.4.2": {} - '@colordx/core@5.5.0': {} + "@colordx/core@5.5.0": {} - '@csstools/selector-resolve-nested@3.1.0(postcss-selector-parser@7.1.4)': + "@csstools/selector-resolve-nested@3.1.0(postcss-selector-parser@7.1.4)": dependencies: postcss-selector-parser: 7.1.4 - '@csstools/selector-specificity@5.0.0(postcss-selector-parser@7.1.4)': + "@csstools/selector-specificity@5.0.0(postcss-selector-parser@7.1.4)": dependencies: postcss-selector-parser: 7.1.4 - '@dxup/nuxt@0.5.6(esbuild@0.28.1)(magicast@0.5.4)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@25.5.0)(esbuild@0.28.1)(jiti@2.7.0)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.49.2)(yaml@2.9.0))': + "@dxup/nuxt@0.5.6(esbuild@0.28.1)(magicast@0.5.4)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@25.5.0)(esbuild@0.28.1)(jiti@2.7.0)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.49.2)(yaml@2.9.0))": dependencies: - '@dxup/unimport': 0.1.2 - '@nuxt/kit': 4.5.1(magic-string@1.1.0)(magicast@0.5.4)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.28.1)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@25.5.0)(esbuild@0.28.1)(jiti@2.7.0)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.49.2)(yaml@2.9.0))) - '@vue/compiler-dom': 3.5.41 + "@dxup/unimport": 0.1.2 + "@nuxt/kit": 4.5.1(magic-string@1.1.0)(magicast@0.5.4)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.28.1)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@25.5.0)(esbuild@0.28.1)(jiti@2.7.0)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.49.2)(yaml@2.9.0))) + "@vue/compiler-dom": 3.5.41 chokidar: 5.0.0 knitwork: 1.3.0 magic-string: 1.1.0 @@ -4473,8 +7134,8 @@ snapshots: tinyglobby: 0.2.17 unplugin: 3.3.0(esbuild@0.28.1)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@25.5.0)(esbuild@0.28.1)(jiti@2.7.0)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.49.2)(yaml@2.9.0)) transitivePeerDependencies: - - '@farmfe/core' - - '@rspack/core' + - "@farmfe/core" + - "@rspack/core" - bun-types-no-globals - esbuild - magicast @@ -4485,98 +7146,98 @@ snapshots: - vite - webpack - '@dxup/unimport@0.1.2': {} + "@dxup/unimport@0.1.2": {} - '@esbuild/aix-ppc64@0.28.1': + "@esbuild/aix-ppc64@0.28.1": optional: true - '@esbuild/android-arm64@0.28.1': + "@esbuild/android-arm64@0.28.1": optional: true - '@esbuild/android-arm@0.28.1': + "@esbuild/android-arm@0.28.1": optional: true - '@esbuild/android-x64@0.28.1': + "@esbuild/android-x64@0.28.1": optional: true - '@esbuild/darwin-arm64@0.28.1': + "@esbuild/darwin-arm64@0.28.1": optional: true - '@esbuild/darwin-x64@0.28.1': + "@esbuild/darwin-x64@0.28.1": optional: true - '@esbuild/freebsd-arm64@0.28.1': + "@esbuild/freebsd-arm64@0.28.1": optional: true - '@esbuild/freebsd-x64@0.28.1': + "@esbuild/freebsd-x64@0.28.1": optional: true - '@esbuild/linux-arm64@0.28.1': + "@esbuild/linux-arm64@0.28.1": optional: true - '@esbuild/linux-arm@0.28.1': + "@esbuild/linux-arm@0.28.1": optional: true - '@esbuild/linux-ia32@0.28.1': + "@esbuild/linux-ia32@0.28.1": optional: true - '@esbuild/linux-loong64@0.28.1': + "@esbuild/linux-loong64@0.28.1": optional: true - '@esbuild/linux-mips64el@0.28.1': + "@esbuild/linux-mips64el@0.28.1": optional: true - '@esbuild/linux-ppc64@0.28.1': + "@esbuild/linux-ppc64@0.28.1": optional: true - '@esbuild/linux-riscv64@0.28.1': + "@esbuild/linux-riscv64@0.28.1": optional: true - '@esbuild/linux-s390x@0.28.1': + "@esbuild/linux-s390x@0.28.1": optional: true - '@esbuild/linux-x64@0.28.1': + "@esbuild/linux-x64@0.28.1": optional: true - '@esbuild/netbsd-arm64@0.28.1': + "@esbuild/netbsd-arm64@0.28.1": optional: true - '@esbuild/netbsd-x64@0.28.1': + "@esbuild/netbsd-x64@0.28.1": optional: true - '@esbuild/openbsd-arm64@0.28.1': + "@esbuild/openbsd-arm64@0.28.1": optional: true - '@esbuild/openbsd-x64@0.28.1': + "@esbuild/openbsd-x64@0.28.1": optional: true - '@esbuild/openharmony-arm64@0.28.1': + "@esbuild/openharmony-arm64@0.28.1": optional: true - '@esbuild/sunos-x64@0.28.1': + "@esbuild/sunos-x64@0.28.1": optional: true - '@esbuild/win32-arm64@0.28.1': + "@esbuild/win32-arm64@0.28.1": optional: true - '@esbuild/win32-ia32@0.28.1': + "@esbuild/win32-ia32@0.28.1": optional: true - '@esbuild/win32-x64@0.28.1': + "@esbuild/win32-x64@0.28.1": optional: true - '@headlessui/vue@1.7.23(vue@3.5.41(typescript@5.9.3))': + "@headlessui/vue@1.7.23(vue@3.5.41(typescript@5.9.3))": dependencies: - '@tanstack/vue-virtual': 3.13.12(vue@3.5.41(typescript@5.9.3)) + "@tanstack/vue-virtual": 3.13.12(vue@3.5.41(typescript@5.9.3)) vue: 3.5.41(typescript@5.9.3) - '@heroicons/vue@2.2.0(vue@3.5.41(typescript@5.9.3))': + "@heroicons/vue@2.2.0(vue@3.5.41(typescript@5.9.3))": dependencies: vue: 3.5.41(typescript@5.9.3) - '@ioredis/commands@1.10.0': {} + "@ioredis/commands@1.10.0": {} - '@isaacs/cliui@8.0.2': + "@isaacs/cliui@8.0.2": dependencies: string-width: 5.1.2 string-width-cjs: string-width@4.2.3 @@ -4585,35 +7246,35 @@ snapshots: wrap-ansi: 8.1.0 wrap-ansi-cjs: wrap-ansi@7.0.0 - '@isaacs/fs-minipass@4.0.1': + "@isaacs/fs-minipass@4.0.1": dependencies: minipass: 7.1.3 - '@jridgewell/gen-mapping@0.3.13': + "@jridgewell/gen-mapping@0.3.13": dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 - '@jridgewell/trace-mapping': 0.3.31 + "@jridgewell/sourcemap-codec": 1.5.5 + "@jridgewell/trace-mapping": 0.3.31 - '@jridgewell/remapping@2.3.5': + "@jridgewell/remapping@2.3.5": dependencies: - '@jridgewell/gen-mapping': 0.3.13 - '@jridgewell/trace-mapping': 0.3.31 + "@jridgewell/gen-mapping": 0.3.13 + "@jridgewell/trace-mapping": 0.3.31 - '@jridgewell/resolve-uri@3.1.2': {} + "@jridgewell/resolve-uri@3.1.2": {} - '@jridgewell/source-map@0.3.11': + "@jridgewell/source-map@0.3.11": dependencies: - '@jridgewell/gen-mapping': 0.3.13 - '@jridgewell/trace-mapping': 0.3.31 + "@jridgewell/gen-mapping": 0.3.13 + "@jridgewell/trace-mapping": 0.3.31 - '@jridgewell/sourcemap-codec@1.5.5': {} + "@jridgewell/sourcemap-codec@1.5.5": {} - '@jridgewell/trace-mapping@0.3.31': + "@jridgewell/trace-mapping@0.3.31": dependencies: - '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.5 + "@jridgewell/resolve-uri": 3.1.2 + "@jridgewell/sourcemap-codec": 1.5.5 - '@koa/router@12.0.2': + "@koa/router@12.0.2": dependencies: debug: 4.4.3 http-errors: 2.0.1 @@ -4623,15 +7284,15 @@ snapshots: transitivePeerDependencies: - supports-color - '@kwsites/file-exists@1.1.1': + "@kwsites/file-exists@1.1.1": dependencies: debug: 4.4.3 transitivePeerDependencies: - supports-color - '@kwsites/promise-deferred@1.1.1': {} + "@kwsites/promise-deferred@1.1.1": {} - '@mapbox/node-pre-gyp@2.0.3': + "@mapbox/node-pre-gyp@2.0.3": dependencies: consola: 3.4.2 detect-libc: 2.1.2 @@ -4644,25 +7305,25 @@ snapshots: - encoding - supports-color - '@napi-rs/lzma-linux-x64-gnu@1.5.1': + "@napi-rs/lzma-linux-x64-gnu@1.5.1": optional: true - '@nodelib/fs.scandir@2.1.5': + "@nodelib/fs.scandir@2.1.5": dependencies: - '@nodelib/fs.stat': 2.0.5 + "@nodelib/fs.stat": 2.0.5 run-parallel: 1.2.0 - '@nodelib/fs.stat@2.0.5': {} + "@nodelib/fs.stat@2.0.5": {} - '@nodelib/fs.walk@1.2.8': + "@nodelib/fs.walk@1.2.8": dependencies: - '@nodelib/fs.scandir': 2.1.5 + "@nodelib/fs.scandir": 2.1.5 fastq: 1.19.1 - '@nuxt/cli@3.37.0(@nuxt/schema@4.5.1)(@parcel/watcher@2.5.6)(cac@6.7.14)(magicast@0.5.4)': + "@nuxt/cli@3.37.0(@nuxt/schema@4.5.1)(@parcel/watcher@2.5.6)(cac@6.7.14)(magicast@0.5.4)": dependencies: - '@bomb.sh/tab': 0.0.19(cac@6.7.14)(citty@0.2.2) - '@clack/prompts': 1.7.0 + "@bomb.sh/tab": 0.0.19(cac@6.7.14)(citty@0.2.2) + "@clack/prompts": 1.7.0 c12: 3.3.4(magicast@0.5.4) citty: 0.2.2 confbox: 0.2.4 @@ -4690,19 +7351,19 @@ snapshots: ufo: 1.6.4 youch: 4.1.1 optionalDependencies: - '@nuxt/schema': 4.5.1 + "@nuxt/schema": 4.5.1 transitivePeerDependencies: - - '@parcel/watcher' + - "@parcel/watcher" - cac - commander - magicast - supports-color - '@nuxt/devalue@2.0.2': {} + "@nuxt/devalue@2.0.2": {} - '@nuxt/devtools-kit@3.4.1(magic-string@1.1.0)(magicast@0.5.4)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.28.1)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@25.5.0)(esbuild@0.28.1)(jiti@2.7.0)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.49.2)(yaml@2.9.0)))(vite@8.2.1(@types/node@25.5.0)(esbuild@0.28.1)(jiti@2.7.0)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.49.2)(yaml@2.9.0))': + "@nuxt/devtools-kit@3.4.1(magic-string@1.1.0)(magicast@0.5.4)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.28.1)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@25.5.0)(esbuild@0.28.1)(jiti@2.7.0)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.49.2)(yaml@2.9.0)))(vite@8.2.1(@types/node@25.5.0)(esbuild@0.28.1)(jiti@2.7.0)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.49.2)(yaml@2.9.0))": dependencies: - '@nuxt/kit': 4.5.1(magic-string@1.1.0)(magicast@0.5.4)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.28.1)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@25.5.0)(esbuild@0.28.1)(jiti@2.7.0)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.49.2)(yaml@2.9.0))) + "@nuxt/kit": 4.5.1(magic-string@1.1.0)(magicast@0.5.4)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.28.1)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@25.5.0)(esbuild@0.28.1)(jiti@2.7.0)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.49.2)(yaml@2.9.0))) execa: 8.0.1 vite: 8.2.1(@types/node@25.5.0)(esbuild@0.28.1)(jiti@2.7.0)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.49.2)(yaml@2.9.0) transitivePeerDependencies: @@ -4712,9 +7373,9 @@ snapshots: - rolldown - unplugin - '@nuxt/devtools-wizard@3.4.1': + "@nuxt/devtools-wizard@3.4.1": dependencies: - '@clack/prompts': 1.7.0 + "@clack/prompts": 1.7.0 consola: 3.4.2 diff: 8.0.4 execa: 8.0.1 @@ -4723,13 +7384,13 @@ snapshots: pkg-types: 2.3.1 semver: 7.8.5 - '@nuxt/devtools@3.4.1(db0@0.3.4)(ioredis@5.11.1)(magic-string@1.1.0)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.28.1)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@25.5.0)(esbuild@0.28.1)(jiti@2.7.0)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.49.2)(yaml@2.9.0)))(vite@8.2.1(@types/node@25.5.0)(esbuild@0.28.1)(jiti@2.7.0)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.49.2)(yaml@2.9.0))(vue@3.5.41(typescript@5.9.3))': + "@nuxt/devtools@3.4.1(db0@0.3.4)(ioredis@5.11.1)(magic-string@1.1.0)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.28.1)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@25.5.0)(esbuild@0.28.1)(jiti@2.7.0)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.49.2)(yaml@2.9.0)))(vite@8.2.1(@types/node@25.5.0)(esbuild@0.28.1)(jiti@2.7.0)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.49.2)(yaml@2.9.0))(vue@3.5.41(typescript@5.9.3))": dependencies: - '@nuxt/devtools-kit': 3.4.1(magic-string@1.1.0)(magicast@0.5.4)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.28.1)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@25.5.0)(esbuild@0.28.1)(jiti@2.7.0)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.49.2)(yaml@2.9.0)))(vite@8.2.1(@types/node@25.5.0)(esbuild@0.28.1)(jiti@2.7.0)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.49.2)(yaml@2.9.0)) - '@nuxt/devtools-wizard': 3.4.1 - '@nuxt/kit': 4.5.1(magic-string@1.1.0)(magicast@0.5.4)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.28.1)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@25.5.0)(esbuild@0.28.1)(jiti@2.7.0)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.49.2)(yaml@2.9.0))) - '@vue/devtools-core': 8.2.1(vue@3.5.41(typescript@5.9.3)) - '@vue/devtools-kit': 8.2.1 + "@nuxt/devtools-kit": 3.4.1(magic-string@1.1.0)(magicast@0.5.4)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.28.1)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@25.5.0)(esbuild@0.28.1)(jiti@2.7.0)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.49.2)(yaml@2.9.0)))(vite@8.2.1(@types/node@25.5.0)(esbuild@0.28.1)(jiti@2.7.0)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.49.2)(yaml@2.9.0)) + "@nuxt/devtools-wizard": 3.4.1 + "@nuxt/kit": 4.5.1(magic-string@1.1.0)(magicast@0.5.4)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.28.1)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@25.5.0)(esbuild@0.28.1)(jiti@2.7.0)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.49.2)(yaml@2.9.0))) + "@vue/devtools-core": 8.2.1(vue@3.5.41(typescript@5.9.3)) + "@vue/devtools-kit": 8.2.1 birpc: 4.0.0 consola: 3.4.2 destr: 2.0.5 @@ -4760,20 +7421,20 @@ snapshots: which: 6.0.1 ws: 8.21.3 transitivePeerDependencies: - - '@azure/app-configuration' - - '@azure/cosmos' - - '@azure/data-tables' - - '@azure/identity' - - '@azure/keyvault-secrets' - - '@azure/storage-blob' - - '@capacitor/preferences' - - '@deno/kv' - - '@netlify/blobs' - - '@planetscale/database' - - '@upstash/redis' - - '@vercel/blob' - - '@vercel/functions' - - '@vercel/kv' + - "@azure/app-configuration" + - "@azure/cosmos" + - "@azure/data-tables" + - "@azure/identity" + - "@azure/keyvault-secrets" + - "@azure/storage-blob" + - "@capacitor/preferences" + - "@deno/kv" + - "@netlify/blobs" + - "@planetscale/database" + - "@upstash/redis" + - "@vercel/blob" + - "@vercel/functions" + - "@vercel/kv" - aws4fetch - bufferutil - db0 @@ -4788,7 +7449,7 @@ snapshots: - utf-8-validate - vue - '@nuxt/kit@3.20.1(magicast@0.5.4)': + "@nuxt/kit@3.20.1(magicast@0.5.4)": dependencies: c12: 3.3.4(magicast@0.5.4) consola: 3.4.2 @@ -4814,7 +7475,7 @@ snapshots: transitivePeerDependencies: - magicast - '@nuxt/kit@4.5.1(magic-string@1.1.0)(magicast@0.5.4)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.28.1)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@25.5.0)(esbuild@0.28.1)(jiti@2.7.0)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.49.2)(yaml@2.9.0)))': + "@nuxt/kit@4.5.1(magic-string@1.1.0)(magicast@0.5.4)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.28.1)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@25.5.0)(esbuild@0.28.1)(jiti@2.7.0)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.49.2)(yaml@2.9.0)))": dependencies: c12: 3.3.4(magicast@0.5.4) consola: 3.4.2 @@ -4844,12 +7505,12 @@ snapshots: - rolldown - unplugin - '@nuxt/nitro-server@4.5.1(a53a6c67a4523b83f74d520e49e406c5)': + "@nuxt/nitro-server@4.5.1(a53a6c67a4523b83f74d520e49e406c5)": dependencies: - '@nuxt/devalue': 2.0.2 - '@nuxt/kit': 4.5.1(magic-string@1.1.0)(magicast@0.5.4)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.28.1)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@25.5.0)(esbuild@0.28.1)(jiti@2.7.0)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.49.2)(yaml@2.9.0))) - '@unhead/vue': 3.3.1(@oxc-project/types@0.143.0)(esbuild@0.28.1)(lightningcss@1.33.0)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@25.5.0)(esbuild@0.28.1)(jiti@2.7.0)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.49.2)(yaml@2.9.0))(vue@3.5.41(typescript@5.9.3)) - '@vue/shared': 3.5.41 + "@nuxt/devalue": 2.0.2 + "@nuxt/kit": 4.5.1(magic-string@1.1.0)(magicast@0.5.4)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.28.1)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@25.5.0)(esbuild@0.28.1)(jiti@2.7.0)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.49.2)(yaml@2.9.0))) + "@unhead/vue": 3.3.1(@oxc-project/types@0.143.0)(esbuild@0.28.1)(lightningcss@1.33.0)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@25.5.0)(esbuild@0.28.1)(jiti@2.7.0)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.49.2)(yaml@2.9.0))(vue@3.5.41(typescript@5.9.3)) + "@vue/shared": 3.5.41 consola: 3.4.2 defu: 6.1.7 destr: 2.0.5 @@ -4876,30 +7537,30 @@ snapshots: vue-bundle-renderer: 2.3.1 vue-devtools-stub: 0.1.0 optionalDependencies: - '@babel/plugin-syntax-typescript': 7.29.7(@babel/core@7.29.7) + "@babel/plugin-syntax-typescript": 7.29.7(@babel/core@7.29.7) transitivePeerDependencies: - - '@azure/app-configuration' - - '@azure/cosmos' - - '@azure/data-tables' - - '@azure/identity' - - '@azure/keyvault-secrets' - - '@azure/storage-blob' - - '@capacitor/preferences' - - '@deno/kv' - - '@electric-sql/pglite' - - '@farmfe/core' - - '@libsql/client' - - '@netlify/blobs' - - '@oxc-project/types' - - '@parcel/watcher' - - '@planetscale/database' - - '@rspack/core' - - '@unhead/cli' - - '@upstash/redis' - - '@vercel/blob' - - '@vercel/functions' - - '@vercel/kv' - - '@vitejs/devtools-kit' + - "@azure/app-configuration" + - "@azure/cosmos" + - "@azure/data-tables" + - "@azure/identity" + - "@azure/keyvault-secrets" + - "@azure/storage-blob" + - "@capacitor/preferences" + - "@deno/kv" + - "@electric-sql/pglite" + - "@farmfe/core" + - "@libsql/client" + - "@netlify/blobs" + - "@oxc-project/types" + - "@parcel/watcher" + - "@planetscale/database" + - "@rspack/core" + - "@unhead/cli" + - "@upstash/redis" + - "@vercel/blob" + - "@vercel/functions" + - "@vercel/kv" + - "@vitejs/devtools-kit" - aws4fetch - bare-abort-controller - bare-buffer @@ -4930,29 +7591,29 @@ snapshots: - webpack - xml2js - '@nuxt/schema@4.5.1': + "@nuxt/schema@4.5.1": dependencies: - '@vue/shared': 3.5.41 + "@vue/shared": 3.5.41 defu: 6.1.7 nostics: 1.2.0 pathe: 2.0.3 pkg-types: 2.3.1 std-env: 4.2.0 - '@nuxt/telemetry@2.8.0(@nuxt/kit@4.5.1(magic-string@1.1.0)(magicast@0.5.4)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.28.1)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@25.5.0)(esbuild@0.28.1)(jiti@2.7.0)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.49.2)(yaml@2.9.0))))': + "@nuxt/telemetry@2.8.0(@nuxt/kit@4.5.1(magic-string@1.1.0)(magicast@0.5.4)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.28.1)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@25.5.0)(esbuild@0.28.1)(jiti@2.7.0)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.49.2)(yaml@2.9.0))))": dependencies: - '@nuxt/kit': 4.5.1(magic-string@1.1.0)(magicast@0.5.4)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.28.1)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@25.5.0)(esbuild@0.28.1)(jiti@2.7.0)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.49.2)(yaml@2.9.0))) + "@nuxt/kit": 4.5.1(magic-string@1.1.0)(magicast@0.5.4)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.28.1)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@25.5.0)(esbuild@0.28.1)(jiti@2.7.0)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.49.2)(yaml@2.9.0))) citty: 0.2.2 consola: 3.4.2 ofetch: 2.0.0-alpha.3 rc9: 3.0.1 std-env: 4.2.0 - '@nuxt/vite-builder@4.5.1(b0892b6506e328e14d8d4e8927b03cbc)': + "@nuxt/vite-builder@4.5.1(b0892b6506e328e14d8d4e8927b03cbc)": dependencies: - '@nuxt/kit': 4.5.1(magic-string@1.1.0)(magicast@0.5.4)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.28.1)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@25.5.0)(esbuild@0.28.1)(jiti@2.7.0)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.49.2)(yaml@2.9.0))) - '@vitejs/plugin-vue': 6.0.8(vite@8.2.1(@types/node@25.5.0)(esbuild@0.28.1)(jiti@2.7.0)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.49.2)(yaml@2.9.0))(vue@3.5.41(typescript@5.9.3)) - '@vitejs/plugin-vue-jsx': 5.1.6(vite@8.2.1(@types/node@25.5.0)(esbuild@0.28.1)(jiti@2.7.0)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.49.2)(yaml@2.9.0))(vue@3.5.41(typescript@5.9.3)) + "@nuxt/kit": 4.5.1(magic-string@1.1.0)(magicast@0.5.4)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.28.1)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@25.5.0)(esbuild@0.28.1)(jiti@2.7.0)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.49.2)(yaml@2.9.0))) + "@vitejs/plugin-vue": 6.0.8(vite@8.2.1(@types/node@25.5.0)(esbuild@0.28.1)(jiti@2.7.0)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.49.2)(yaml@2.9.0))(vue@3.5.41(typescript@5.9.3)) + "@vitejs/plugin-vue-jsx": 5.1.6(vite@8.2.1(@types/node@25.5.0)(esbuild@0.28.1)(jiti@2.7.0)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.49.2)(yaml@2.9.0))(vue@3.5.41(typescript@5.9.3)) autoprefixer: 10.5.4(postcss@8.5.26) consola: 3.4.2 cssnano: 8.0.4(postcss@8.5.26) @@ -4982,13 +7643,13 @@ snapshots: vue: 3.5.41(typescript@5.9.3) vue-bundle-renderer: 2.3.1 optionalDependencies: - '@babel/plugin-syntax-jsx': 7.29.7(@babel/core@7.29.7) + "@babel/plugin-syntax-jsx": 7.29.7(@babel/core@7.29.7) rolldown: 1.2.3 rollup-plugin-visualizer: 7.0.1(rolldown@1.2.3)(rollup@4.62.4) transitivePeerDependencies: - - '@biomejs/biome' - - '@types/node' - - '@vitejs/devtools' + - "@biomejs/biome" + - "@types/node" + - "@vitejs/devtools" - esbuild - eslint - less @@ -5011,9 +7672,9 @@ snapshots: - vue-tsc - yaml - '@nuxtjs/tailwindcss@6.14.0(magicast@0.5.4)(yaml@2.9.0)': + "@nuxtjs/tailwindcss@6.14.0(magicast@0.5.4)(yaml@2.9.0)": dependencies: - '@nuxt/kit': 3.20.1(magicast@0.5.4) + "@nuxt/kit": 3.20.1(magicast@0.5.4) autoprefixer: 10.5.0(postcss@8.5.24) c12: 3.3.4(magicast@0.5.4) consola: 3.4.2 @@ -5035,142 +7696,142 @@ snapshots: - tsx - yaml - '@oxc-project/types@0.143.0': {} + "@oxc-project/types@0.143.0": {} - '@parcel/watcher-android-arm64@2.5.6': + "@parcel/watcher-android-arm64@2.5.6": optional: true - '@parcel/watcher-darwin-arm64@2.5.6': + "@parcel/watcher-darwin-arm64@2.5.6": optional: true - '@parcel/watcher-darwin-x64@2.5.6': + "@parcel/watcher-darwin-x64@2.5.6": optional: true - '@parcel/watcher-freebsd-x64@2.5.6': + "@parcel/watcher-freebsd-x64@2.5.6": optional: true - '@parcel/watcher-linux-arm-glibc@2.5.6': + "@parcel/watcher-linux-arm-glibc@2.5.6": optional: true - '@parcel/watcher-linux-arm-musl@2.5.6': + "@parcel/watcher-linux-arm-musl@2.5.6": optional: true - '@parcel/watcher-linux-arm64-glibc@2.5.6': + "@parcel/watcher-linux-arm64-glibc@2.5.6": optional: true - '@parcel/watcher-linux-arm64-musl@2.5.6': + "@parcel/watcher-linux-arm64-musl@2.5.6": optional: true - '@parcel/watcher-linux-x64-glibc@2.5.6': + "@parcel/watcher-linux-x64-glibc@2.5.6": optional: true - '@parcel/watcher-linux-x64-musl@2.5.6': + "@parcel/watcher-linux-x64-musl@2.5.6": optional: true - '@parcel/watcher-wasm@2.6.0': + "@parcel/watcher-wasm@2.6.0": dependencies: is-glob: 4.0.3 picomatch: 4.0.5 - '@parcel/watcher-win32-arm64@2.5.6': + "@parcel/watcher-win32-arm64@2.5.6": optional: true - '@parcel/watcher-win32-ia32@2.5.6': + "@parcel/watcher-win32-ia32@2.5.6": optional: true - '@parcel/watcher-win32-x64@2.5.6': + "@parcel/watcher-win32-x64@2.5.6": optional: true - '@parcel/watcher@2.5.6': + "@parcel/watcher@2.5.6": dependencies: detect-libc: 2.1.2 is-glob: 4.0.3 node-addon-api: 7.1.1 picomatch: 4.0.5 optionalDependencies: - '@parcel/watcher-android-arm64': 2.5.6 - '@parcel/watcher-darwin-arm64': 2.5.6 - '@parcel/watcher-darwin-x64': 2.5.6 - '@parcel/watcher-freebsd-x64': 2.5.6 - '@parcel/watcher-linux-arm-glibc': 2.5.6 - '@parcel/watcher-linux-arm-musl': 2.5.6 - '@parcel/watcher-linux-arm64-glibc': 2.5.6 - '@parcel/watcher-linux-arm64-musl': 2.5.6 - '@parcel/watcher-linux-x64-glibc': 2.5.6 - '@parcel/watcher-linux-x64-musl': 2.5.6 - '@parcel/watcher-win32-arm64': 2.5.6 - '@parcel/watcher-win32-ia32': 2.5.6 - '@parcel/watcher-win32-x64': 2.5.6 + "@parcel/watcher-android-arm64": 2.5.6 + "@parcel/watcher-darwin-arm64": 2.5.6 + "@parcel/watcher-darwin-x64": 2.5.6 + "@parcel/watcher-freebsd-x64": 2.5.6 + "@parcel/watcher-linux-arm-glibc": 2.5.6 + "@parcel/watcher-linux-arm-musl": 2.5.6 + "@parcel/watcher-linux-arm64-glibc": 2.5.6 + "@parcel/watcher-linux-arm64-musl": 2.5.6 + "@parcel/watcher-linux-x64-glibc": 2.5.6 + "@parcel/watcher-linux-x64-musl": 2.5.6 + "@parcel/watcher-win32-arm64": 2.5.6 + "@parcel/watcher-win32-ia32": 2.5.6 + "@parcel/watcher-win32-x64": 2.5.6 optional: true - '@pkgjs/parseargs@0.11.0': + "@pkgjs/parseargs@0.11.0": optional: true - '@polka/url@1.0.0-next.29': {} + "@polka/url@1.0.0-next.29": {} - '@poppinss/colors@4.1.6': + "@poppinss/colors@4.1.6": dependencies: kleur: 4.1.5 - '@poppinss/dumper@0.7.0': + "@poppinss/dumper@0.7.0": dependencies: - '@poppinss/colors': 4.1.6 - '@sindresorhus/is': 7.2.0 + "@poppinss/colors": 4.1.6 + "@sindresorhus/is": 7.2.0 supports-color: 10.2.2 - '@poppinss/exception@1.2.3': {} + "@poppinss/exception@1.2.3": {} - '@rolldown/binding-android-arm64@1.2.3': + "@rolldown/binding-android-arm64@1.2.3": optional: true - '@rolldown/binding-darwin-arm64@1.2.3': + "@rolldown/binding-darwin-arm64@1.2.3": optional: true - '@rolldown/binding-darwin-x64@1.2.3': + "@rolldown/binding-darwin-x64@1.2.3": optional: true - '@rolldown/binding-freebsd-x64@1.2.3': + "@rolldown/binding-freebsd-x64@1.2.3": optional: true - '@rolldown/binding-linux-arm-gnueabihf@1.2.3': + "@rolldown/binding-linux-arm-gnueabihf@1.2.3": optional: true - '@rolldown/binding-linux-arm64-gnu@1.2.3': + "@rolldown/binding-linux-arm64-gnu@1.2.3": optional: true - '@rolldown/binding-linux-arm64-musl@1.2.3': + "@rolldown/binding-linux-arm64-musl@1.2.3": optional: true - '@rolldown/binding-linux-ppc64-gnu@1.2.3': + "@rolldown/binding-linux-ppc64-gnu@1.2.3": optional: true - '@rolldown/binding-linux-s390x-gnu@1.2.3': + "@rolldown/binding-linux-s390x-gnu@1.2.3": optional: true - '@rolldown/binding-linux-x64-gnu@1.2.3': + "@rolldown/binding-linux-x64-gnu@1.2.3": optional: true - '@rolldown/binding-linux-x64-musl@1.2.3': + "@rolldown/binding-linux-x64-musl@1.2.3": optional: true - '@rolldown/binding-openharmony-arm64@1.2.3': + "@rolldown/binding-openharmony-arm64@1.2.3": optional: true - '@rolldown/binding-win32-arm64-msvc@1.2.3': + "@rolldown/binding-win32-arm64-msvc@1.2.3": optional: true - '@rolldown/binding-win32-x64-msvc@1.2.3': + "@rolldown/binding-win32-x64-msvc@1.2.3": optional: true - '@rolldown/pluginutils@1.0.1': {} + "@rolldown/pluginutils@1.0.1": {} - '@rollup/plugin-alias@6.0.0(rollup@4.62.4)': + "@rollup/plugin-alias@6.0.0(rollup@4.62.4)": optionalDependencies: rollup: 4.62.4 - '@rollup/plugin-commonjs@29.0.3(rollup@4.62.4)': + "@rollup/plugin-commonjs@29.0.3(rollup@4.62.4)": dependencies: - '@rollup/pluginutils': 5.4.0(rollup@4.62.4) + "@rollup/pluginutils": 5.4.0(rollup@4.62.4) commondir: 1.0.1 estree-walker: 2.0.2 fdir: 6.5.0(picomatch@4.0.5) @@ -5180,38 +7841,38 @@ snapshots: optionalDependencies: rollup: 4.62.4 - '@rollup/plugin-inject@5.0.5(rollup@4.62.4)': + "@rollup/plugin-inject@5.0.5(rollup@4.62.4)": dependencies: - '@rollup/pluginutils': 5.4.0(rollup@4.62.4) + "@rollup/pluginutils": 5.4.0(rollup@4.62.4) estree-walker: 2.0.2 magic-string: 0.30.21 optionalDependencies: rollup: 4.62.4 - '@rollup/plugin-json@6.1.0(rollup@4.62.4)': + "@rollup/plugin-json@6.1.0(rollup@4.62.4)": dependencies: - '@rollup/pluginutils': 5.4.0(rollup@4.62.4) + "@rollup/pluginutils": 5.4.0(rollup@4.62.4) optionalDependencies: rollup: 4.62.4 - '@rollup/plugin-node-resolve@16.0.3(rollup@4.62.4)': + "@rollup/plugin-node-resolve@16.0.3(rollup@4.62.4)": dependencies: - '@rollup/pluginutils': 5.4.0(rollup@4.62.4) - '@types/resolve': 1.20.2 + "@rollup/pluginutils": 5.4.0(rollup@4.62.4) + "@types/resolve": 1.20.2 deepmerge: 4.3.1 is-module: 1.0.0 resolve: 1.22.12 optionalDependencies: rollup: 4.62.4 - '@rollup/plugin-replace@6.0.3(rollup@4.62.4)': + "@rollup/plugin-replace@6.0.3(rollup@4.62.4)": dependencies: - '@rollup/pluginutils': 5.4.0(rollup@4.62.4) + "@rollup/pluginutils": 5.4.0(rollup@4.62.4) magic-string: 0.30.21 optionalDependencies: rollup: 4.62.4 - '@rollup/plugin-terser@1.0.0(rollup@4.62.4)': + "@rollup/plugin-terser@1.0.0(rollup@4.62.4)": dependencies: serialize-javascript: 7.0.7 smob: 1.6.2 @@ -5219,158 +7880,158 @@ snapshots: optionalDependencies: rollup: 4.62.4 - '@rollup/pluginutils@5.4.0(rollup@4.62.4)': + "@rollup/pluginutils@5.4.0(rollup@4.62.4)": dependencies: - '@types/estree': 1.0.9 + "@types/estree": 1.0.9 estree-walker: 2.0.2 picomatch: 4.0.5 optionalDependencies: rollup: 4.62.4 - '@rollup/rollup-android-arm-eabi@4.62.4': + "@rollup/rollup-android-arm-eabi@4.62.4": optional: true - '@rollup/rollup-android-arm64@4.62.4': + "@rollup/rollup-android-arm64@4.62.4": optional: true - '@rollup/rollup-darwin-arm64@4.62.4': + "@rollup/rollup-darwin-arm64@4.62.4": optional: true - '@rollup/rollup-darwin-x64@4.62.4': + "@rollup/rollup-darwin-x64@4.62.4": optional: true - '@rollup/rollup-freebsd-arm64@4.62.4': + "@rollup/rollup-freebsd-arm64@4.62.4": optional: true - '@rollup/rollup-freebsd-x64@4.62.4': + "@rollup/rollup-freebsd-x64@4.62.4": optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.62.4': + "@rollup/rollup-linux-arm-gnueabihf@4.62.4": optional: true - '@rollup/rollup-linux-arm-musleabihf@4.62.4': + "@rollup/rollup-linux-arm-musleabihf@4.62.4": optional: true - '@rollup/rollup-linux-arm64-gnu@4.62.4': + "@rollup/rollup-linux-arm64-gnu@4.62.4": optional: true - '@rollup/rollup-linux-arm64-musl@4.62.4': + "@rollup/rollup-linux-arm64-musl@4.62.4": optional: true - '@rollup/rollup-linux-loong64-gnu@4.62.4': + "@rollup/rollup-linux-loong64-gnu@4.62.4": optional: true - '@rollup/rollup-linux-loong64-musl@4.62.4': + "@rollup/rollup-linux-loong64-musl@4.62.4": optional: true - '@rollup/rollup-linux-ppc64-gnu@4.62.4': + "@rollup/rollup-linux-ppc64-gnu@4.62.4": optional: true - '@rollup/rollup-linux-ppc64-musl@4.62.4': + "@rollup/rollup-linux-ppc64-musl@4.62.4": optional: true - '@rollup/rollup-linux-riscv64-gnu@4.62.4': + "@rollup/rollup-linux-riscv64-gnu@4.62.4": optional: true - '@rollup/rollup-linux-riscv64-musl@4.62.4': + "@rollup/rollup-linux-riscv64-musl@4.62.4": optional: true - '@rollup/rollup-linux-s390x-gnu@4.62.4': + "@rollup/rollup-linux-s390x-gnu@4.62.4": optional: true - '@rollup/rollup-linux-x64-gnu@4.62.4': + "@rollup/rollup-linux-x64-gnu@4.62.4": optional: true - '@rollup/rollup-linux-x64-musl@4.62.4': + "@rollup/rollup-linux-x64-musl@4.62.4": optional: true - '@rollup/rollup-openbsd-x64@4.62.4': + "@rollup/rollup-openbsd-x64@4.62.4": optional: true - '@rollup/rollup-openharmony-arm64@4.62.4': + "@rollup/rollup-openharmony-arm64@4.62.4": optional: true - '@rollup/rollup-win32-arm64-msvc@4.62.4': + "@rollup/rollup-win32-arm64-msvc@4.62.4": optional: true - '@rollup/rollup-win32-ia32-msvc@4.62.4': + "@rollup/rollup-win32-ia32-msvc@4.62.4": optional: true - '@rollup/rollup-win32-x64-gnu@4.62.4': + "@rollup/rollup-win32-x64-gnu@4.62.4": optional: true - '@rollup/rollup-win32-x64-msvc@4.62.4': + "@rollup/rollup-win32-x64-msvc@4.62.4": optional: true - '@simple-git/args-pathspec@1.0.3': {} + "@simple-git/args-pathspec@1.0.3": {} - '@simple-git/argv-parser@1.1.1': + "@simple-git/argv-parser@1.1.1": dependencies: - '@simple-git/args-pathspec': 1.0.3 + "@simple-git/args-pathspec": 1.0.3 - '@sindresorhus/is@7.2.0': {} + "@sindresorhus/is@7.2.0": {} - '@sindresorhus/merge-streams@4.0.0': {} + "@sindresorhus/merge-streams@4.0.0": {} - '@speed-highlight/core@1.2.23': {} + "@speed-highlight/core@1.2.23": {} - '@tailwindcss/forms@0.5.10(tailwindcss@3.4.18(yaml@2.9.0))': + "@tailwindcss/forms@0.5.10(tailwindcss@3.4.18(yaml@2.9.0))": dependencies: mini-svg-data-uri: 1.4.4 tailwindcss: 3.4.18(yaml@2.9.0) - '@tailwindcss/typography@0.5.19(tailwindcss@3.4.18(yaml@2.9.0))': + "@tailwindcss/typography@0.5.19(tailwindcss@3.4.18(yaml@2.9.0))": dependencies: postcss-selector-parser: 6.0.10 tailwindcss: 3.4.18(yaml@2.9.0) - '@tanstack/virtual-core@3.13.12': {} + "@tanstack/virtual-core@3.13.12": {} - '@tanstack/vue-virtual@3.13.12(vue@3.5.41(typescript@5.9.3))': + "@tanstack/vue-virtual@3.13.12(vue@3.5.41(typescript@5.9.3))": dependencies: - '@tanstack/virtual-core': 3.13.12 + "@tanstack/virtual-core": 3.13.12 vue: 3.5.41(typescript@5.9.3) - '@tauri-apps/api@2.9.1': {} + "@tauri-apps/api@2.9.1": {} - '@tauri-apps/plugin-dialog@2.6.0': + "@tauri-apps/plugin-dialog@2.6.0": dependencies: - '@tauri-apps/api': 2.9.1 + "@tauri-apps/api": 2.9.1 - '@tauri-apps/plugin-os@2.3.2': + "@tauri-apps/plugin-os@2.3.2": dependencies: - '@tauri-apps/api': 2.9.1 + "@tauri-apps/api": 2.9.1 - '@tauri-apps/plugin-shell@2.3.3': + "@tauri-apps/plugin-shell@2.3.3": dependencies: - '@tauri-apps/api': 2.9.1 + "@tauri-apps/api": 2.9.1 - '@types/debug@4.1.12': + "@types/debug@4.1.12": dependencies: - '@types/ms': 2.1.0 + "@types/ms": 2.1.0 - '@types/estree@1.0.9': {} + "@types/estree@1.0.9": {} - '@types/jsesc@2.5.1': {} + "@types/jsesc@2.5.1": {} - '@types/linkify-it@5.0.0': {} + "@types/linkify-it@5.0.0": {} - '@types/markdown-it@14.1.2': + "@types/markdown-it@14.1.2": dependencies: - '@types/linkify-it': 5.0.0 - '@types/mdurl': 2.0.0 + "@types/linkify-it": 5.0.0 + "@types/mdurl": 2.0.0 - '@types/mdurl@2.0.0': {} + "@types/mdurl@2.0.0": {} - '@types/ms@2.1.0': {} + "@types/ms@2.1.0": {} - '@types/node@25.5.0': + "@types/node@25.5.0": dependencies: undici-types: 7.18.2 - '@types/resolve@1.20.2': {} + "@types/resolve@1.20.2": {} - '@unhead/bundler@3.3.1(@oxc-project/types@0.143.0)(esbuild@0.28.1)(lightningcss@1.33.0)(rolldown@1.2.3)(rollup@4.62.4)(unhead@3.3.1(esbuild@0.28.1)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@25.5.0)(esbuild@0.28.1)(jiti@2.7.0)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.49.2)(yaml@2.9.0)))(vite@8.2.1(@types/node@25.5.0)(esbuild@0.28.1)(jiti@2.7.0)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.49.2)(yaml@2.9.0))': + "@unhead/bundler@3.3.1(@oxc-project/types@0.143.0)(esbuild@0.28.1)(lightningcss@1.33.0)(rolldown@1.2.3)(rollup@4.62.4)(unhead@3.3.1(esbuild@0.28.1)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@25.5.0)(esbuild@0.28.1)(jiti@2.7.0)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.49.2)(yaml@2.9.0)))(vite@8.2.1(@types/node@25.5.0)(esbuild@0.28.1)(jiti@2.7.0)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.49.2)(yaml@2.9.0))": dependencies: magic-string: 1.1.0 oxc-walker: 1.1.1(@oxc-project/types@0.143.0)(rolldown@1.2.3) @@ -5382,16 +8043,16 @@ snapshots: rolldown: 1.2.3 vite: 8.2.1(@types/node@25.5.0)(esbuild@0.28.1)(jiti@2.7.0)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.49.2)(yaml@2.9.0) transitivePeerDependencies: - - '@farmfe/core' - - '@oxc-project/types' - - '@rspack/core' + - "@farmfe/core" + - "@oxc-project/types" + - "@rspack/core" - bun-types-no-globals - rollup - unloader - '@unhead/vue@3.3.1(@oxc-project/types@0.143.0)(esbuild@0.28.1)(lightningcss@1.33.0)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@25.5.0)(esbuild@0.28.1)(jiti@2.7.0)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.49.2)(yaml@2.9.0))(vue@3.5.41(typescript@5.9.3))': + "@unhead/vue@3.3.1(@oxc-project/types@0.143.0)(esbuild@0.28.1)(lightningcss@1.33.0)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@25.5.0)(esbuild@0.28.1)(jiti@2.7.0)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.49.2)(yaml@2.9.0))(vue@3.5.41(typescript@5.9.3))": dependencies: - '@unhead/bundler': 3.3.1(@oxc-project/types@0.143.0)(esbuild@0.28.1)(lightningcss@1.33.0)(rolldown@1.2.3)(rollup@4.62.4)(unhead@3.3.1(esbuild@0.28.1)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@25.5.0)(esbuild@0.28.1)(jiti@2.7.0)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.49.2)(yaml@2.9.0)))(vite@8.2.1(@types/node@25.5.0)(esbuild@0.28.1)(jiti@2.7.0)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.49.2)(yaml@2.9.0)) + "@unhead/bundler": 3.3.1(@oxc-project/types@0.143.0)(esbuild@0.28.1)(lightningcss@1.33.0)(rolldown@1.2.3)(rollup@4.62.4)(unhead@3.3.1(esbuild@0.28.1)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@25.5.0)(esbuild@0.28.1)(jiti@2.7.0)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.49.2)(yaml@2.9.0)))(vite@8.2.1(@types/node@25.5.0)(esbuild@0.28.1)(jiti@2.7.0)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.49.2)(yaml@2.9.0)) hookable: 6.1.1 unhead: 3.3.1(esbuild@0.28.1)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@25.5.0)(esbuild@0.28.1)(jiti@2.7.0)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.49.2)(yaml@2.9.0)) unplugin: 3.3.0(esbuild@0.28.1)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@25.5.0)(esbuild@0.28.1)(jiti@2.7.0)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.49.2)(yaml@2.9.0)) @@ -5399,11 +8060,11 @@ snapshots: optionalDependencies: vite: 8.2.1(@types/node@25.5.0)(esbuild@0.28.1)(jiti@2.7.0)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.49.2)(yaml@2.9.0) transitivePeerDependencies: - - '@farmfe/core' - - '@oxc-project/types' - - '@rspack/core' - - '@unhead/cli' - - '@vitejs/devtools-kit' + - "@farmfe/core" + - "@oxc-project/types" + - "@rspack/core" + - "@unhead/cli" + - "@vitejs/devtools-kit" - bun-types-no-globals - esbuild - lightningcss @@ -5412,10 +8073,10 @@ snapshots: - rollup - unloader - '@vercel/nft@1.10.2(rollup@4.62.4)': + "@vercel/nft@1.10.2(rollup@4.62.4)": dependencies: - '@mapbox/node-pre-gyp': 2.0.3 - '@rollup/pluginutils': 5.4.0(rollup@4.62.4) + "@mapbox/node-pre-gyp": 2.0.3 + "@rollup/pluginutils": 5.4.0(rollup@4.62.4) acorn: 8.18.0 acorn-import-attributes: 1.9.5(acorn@8.18.0) async-sema: 3.1.1 @@ -5431,39 +8092,39 @@ snapshots: - rollup - supports-color - '@vitejs/plugin-vue-jsx@5.1.6(vite@8.2.1(@types/node@25.5.0)(esbuild@0.28.1)(jiti@2.7.0)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.49.2)(yaml@2.9.0))(vue@3.5.41(typescript@5.9.3))': + "@vitejs/plugin-vue-jsx@5.1.6(vite@8.2.1(@types/node@25.5.0)(esbuild@0.28.1)(jiti@2.7.0)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.49.2)(yaml@2.9.0))(vue@3.5.41(typescript@5.9.3))": dependencies: - '@babel/core': 7.29.7 - '@babel/plugin-syntax-typescript': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-typescript': 7.29.7(@babel/core@7.29.7) - '@rolldown/pluginutils': 1.0.1 - '@vue/babel-plugin-jsx': 2.0.1(@babel/core@7.29.7) + "@babel/core": 7.29.7 + "@babel/plugin-syntax-typescript": 7.29.7(@babel/core@7.29.7) + "@babel/plugin-transform-typescript": 7.29.7(@babel/core@7.29.7) + "@rolldown/pluginutils": 1.0.1 + "@vue/babel-plugin-jsx": 2.0.1(@babel/core@7.29.7) vite: 8.2.1(@types/node@25.5.0)(esbuild@0.28.1)(jiti@2.7.0)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.49.2)(yaml@2.9.0) vue: 3.5.41(typescript@5.9.3) transitivePeerDependencies: - supports-color - '@vitejs/plugin-vue@6.0.8(vite@8.2.1(@types/node@25.5.0)(esbuild@0.28.1)(jiti@2.7.0)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.49.2)(yaml@2.9.0))(vue@3.5.41(typescript@5.9.3))': + "@vitejs/plugin-vue@6.0.8(vite@8.2.1(@types/node@25.5.0)(esbuild@0.28.1)(jiti@2.7.0)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.49.2)(yaml@2.9.0))(vue@3.5.41(typescript@5.9.3))": dependencies: - '@rolldown/pluginutils': 1.0.1 + "@rolldown/pluginutils": 1.0.1 vite: 8.2.1(@types/node@25.5.0)(esbuild@0.28.1)(jiti@2.7.0)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.49.2)(yaml@2.9.0) vue: 3.5.41(typescript@5.9.3) - '@volar/language-core@2.4.15': + "@volar/language-core@2.4.15": dependencies: - '@volar/source-map': 2.4.15 + "@volar/source-map": 2.4.15 - '@volar/source-map@2.4.15': {} + "@volar/source-map@2.4.15": {} - '@volar/typescript@2.4.15': + "@volar/typescript@2.4.15": dependencies: - '@volar/language-core': 2.4.15 + "@volar/language-core": 2.4.15 path-browserify: 1.0.1 vscode-uri: 3.1.0 - '@vue-macros/common@3.1.4(vue@3.5.41(typescript@5.9.3))': + "@vue-macros/common@3.1.4(vue@3.5.41(typescript@5.9.3))": dependencies: - '@vue/compiler-sfc': 3.5.40 + "@vue/compiler-sfc": 3.5.40 ast-kit: 2.2.0 local-pkg: 1.2.1 magic-string-ast: 1.0.3 @@ -5471,138 +8132,138 @@ snapshots: optionalDependencies: vue: 3.5.41(typescript@5.9.3) - '@vue/babel-helper-vue-transform-on@2.0.1': {} + "@vue/babel-helper-vue-transform-on@2.0.1": {} - '@vue/babel-plugin-jsx@2.0.1(@babel/core@7.29.7)': + "@vue/babel-plugin-jsx@2.0.1(@babel/core@7.29.7)": dependencies: - '@babel/helper-module-imports': 7.29.7 - '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-syntax-jsx': 7.29.7(@babel/core@7.29.7) - '@babel/template': 7.29.7 - '@babel/traverse': 7.29.8 - '@babel/types': 7.29.8 - '@vue/babel-helper-vue-transform-on': 2.0.1 - '@vue/babel-plugin-resolve-type': 2.0.1(@babel/core@7.29.7) - '@vue/shared': 3.5.41 + "@babel/helper-module-imports": 7.29.7 + "@babel/helper-plugin-utils": 7.29.7 + "@babel/plugin-syntax-jsx": 7.29.7(@babel/core@7.29.7) + "@babel/template": 7.29.7 + "@babel/traverse": 7.29.8 + "@babel/types": 7.29.8 + "@vue/babel-helper-vue-transform-on": 2.0.1 + "@vue/babel-plugin-resolve-type": 2.0.1(@babel/core@7.29.7) + "@vue/shared": 3.5.41 optionalDependencies: - '@babel/core': 7.29.7 + "@babel/core": 7.29.7 transitivePeerDependencies: - supports-color - '@vue/babel-plugin-resolve-type@2.0.1(@babel/core@7.29.7)': + "@vue/babel-plugin-resolve-type@2.0.1(@babel/core@7.29.7)": dependencies: - '@babel/code-frame': 7.29.7 - '@babel/core': 7.29.7 - '@babel/helper-module-imports': 7.29.7 - '@babel/helper-plugin-utils': 7.29.7 - '@babel/parser': 7.29.8 - '@vue/compiler-sfc': 3.5.41 + "@babel/code-frame": 7.29.7 + "@babel/core": 7.29.7 + "@babel/helper-module-imports": 7.29.7 + "@babel/helper-plugin-utils": 7.29.7 + "@babel/parser": 7.29.8 + "@vue/compiler-sfc": 3.5.41 transitivePeerDependencies: - supports-color - '@vue/compiler-core@3.5.38': + "@vue/compiler-core@3.5.38": dependencies: - '@babel/parser': 7.29.8 - '@vue/shared': 3.5.38 + "@babel/parser": 7.29.8 + "@vue/shared": 3.5.38 entities: 7.0.1 estree-walker: 2.0.2 source-map-js: 1.2.1 - '@vue/compiler-core@3.5.40': + "@vue/compiler-core@3.5.40": dependencies: - '@babel/parser': 7.29.8 - '@vue/shared': 3.5.40 + "@babel/parser": 7.29.8 + "@vue/shared": 3.5.40 entities: 7.0.1 estree-walker: 2.0.2 source-map-js: 1.2.1 - '@vue/compiler-core@3.5.41': + "@vue/compiler-core@3.5.41": dependencies: - '@babel/parser': 7.29.8 - '@vue/shared': 3.5.41 + "@babel/parser": 7.29.8 + "@vue/shared": 3.5.41 entities: 7.0.1 estree-walker: 2.0.2 source-map-js: 1.2.1 - '@vue/compiler-dom@3.5.38': + "@vue/compiler-dom@3.5.38": dependencies: - '@vue/compiler-core': 3.5.38 - '@vue/shared': 3.5.38 + "@vue/compiler-core": 3.5.38 + "@vue/shared": 3.5.38 - '@vue/compiler-dom@3.5.40': + "@vue/compiler-dom@3.5.40": dependencies: - '@vue/compiler-core': 3.5.40 - '@vue/shared': 3.5.40 + "@vue/compiler-core": 3.5.40 + "@vue/shared": 3.5.40 - '@vue/compiler-dom@3.5.41': + "@vue/compiler-dom@3.5.41": dependencies: - '@vue/compiler-core': 3.5.41 - '@vue/shared': 3.5.41 + "@vue/compiler-core": 3.5.41 + "@vue/shared": 3.5.41 - '@vue/compiler-sfc@3.5.40': + "@vue/compiler-sfc@3.5.40": dependencies: - '@babel/parser': 7.29.7 - '@vue/compiler-core': 3.5.40 - '@vue/compiler-dom': 3.5.40 - '@vue/compiler-ssr': 3.5.40 - '@vue/shared': 3.5.40 + "@babel/parser": 7.29.7 + "@vue/compiler-core": 3.5.40 + "@vue/compiler-dom": 3.5.40 + "@vue/compiler-ssr": 3.5.40 + "@vue/shared": 3.5.40 estree-walker: 2.0.2 magic-string: 0.30.21 postcss: 8.5.26 source-map-js: 1.2.1 - '@vue/compiler-sfc@3.5.41': + "@vue/compiler-sfc@3.5.41": dependencies: - '@babel/parser': 7.29.8 - '@vue/compiler-core': 3.5.41 - '@vue/compiler-dom': 3.5.41 - '@vue/compiler-ssr': 3.5.41 - '@vue/shared': 3.5.41 + "@babel/parser": 7.29.8 + "@vue/compiler-core": 3.5.41 + "@vue/compiler-dom": 3.5.41 + "@vue/compiler-ssr": 3.5.41 + "@vue/shared": 3.5.41 estree-walker: 2.0.2 magic-string: 0.30.21 postcss: 8.5.26 source-map-js: 1.2.1 - '@vue/compiler-ssr@3.5.40': + "@vue/compiler-ssr@3.5.40": dependencies: - '@vue/compiler-dom': 3.5.40 - '@vue/shared': 3.5.40 + "@vue/compiler-dom": 3.5.40 + "@vue/shared": 3.5.40 - '@vue/compiler-ssr@3.5.41': + "@vue/compiler-ssr@3.5.41": dependencies: - '@vue/compiler-dom': 3.5.41 - '@vue/shared': 3.5.41 + "@vue/compiler-dom": 3.5.41 + "@vue/shared": 3.5.41 - '@vue/compiler-vue2@2.7.16': + "@vue/compiler-vue2@2.7.16": dependencies: de-indent: 1.0.2 he: 1.2.0 - '@vue/devtools-api@8.2.1': + "@vue/devtools-api@8.2.1": dependencies: - '@vue/devtools-kit': 8.2.1 + "@vue/devtools-kit": 8.2.1 - '@vue/devtools-core@8.2.1(vue@3.5.41(typescript@5.9.3))': + "@vue/devtools-core@8.2.1(vue@3.5.41(typescript@5.9.3))": dependencies: - '@vue/devtools-kit': 8.2.1 - '@vue/devtools-shared': 8.2.1 + "@vue/devtools-kit": 8.2.1 + "@vue/devtools-shared": 8.2.1 vue: 3.5.41(typescript@5.9.3) - '@vue/devtools-kit@8.2.1': + "@vue/devtools-kit@8.2.1": dependencies: - '@vue/devtools-shared': 8.2.1 + "@vue/devtools-shared": 8.2.1 birpc: 2.9.0 hookable: 5.5.3 perfect-debounce: 2.1.0 - '@vue/devtools-shared@8.2.1': {} + "@vue/devtools-shared@8.2.1": {} - '@vue/language-core@2.2.12(typescript@5.9.3)': + "@vue/language-core@2.2.12(typescript@5.9.3)": dependencies: - '@volar/language-core': 2.4.15 - '@vue/compiler-dom': 3.5.38 - '@vue/compiler-vue2': 2.7.16 - '@vue/shared': 3.5.41 + "@volar/language-core": 2.4.15 + "@vue/compiler-dom": 3.5.38 + "@vue/compiler-vue2": 2.7.16 + "@vue/shared": 3.5.41 alien-signals: 1.0.13 minimatch: 9.0.5 muggle-string: 0.4.1 @@ -5610,33 +8271,33 @@ snapshots: optionalDependencies: typescript: 5.9.3 - '@vue/reactivity@3.5.41': + "@vue/reactivity@3.5.41": dependencies: - '@vue/shared': 3.5.41 + "@vue/shared": 3.5.41 - '@vue/runtime-core@3.5.41': + "@vue/runtime-core@3.5.41": dependencies: - '@vue/reactivity': 3.5.41 - '@vue/shared': 3.5.41 + "@vue/reactivity": 3.5.41 + "@vue/shared": 3.5.41 - '@vue/runtime-dom@3.5.41': + "@vue/runtime-dom@3.5.41": dependencies: - '@vue/reactivity': 3.5.41 - '@vue/runtime-core': 3.5.41 - '@vue/shared': 3.5.41 + "@vue/reactivity": 3.5.41 + "@vue/runtime-core": 3.5.41 + "@vue/shared": 3.5.41 csstype: 3.2.3 - '@vue/server-renderer@3.5.41': + "@vue/server-renderer@3.5.41": dependencies: - '@vue/compiler-ssr': 3.5.41 - '@vue/runtime-dom': 3.5.41 - '@vue/shared': 3.5.41 + "@vue/compiler-ssr": 3.5.41 + "@vue/runtime-dom": 3.5.41 + "@vue/shared": 3.5.41 - '@vue/shared@3.5.38': {} + "@vue/shared@3.5.38": {} - '@vue/shared@3.5.40': {} + "@vue/shared@3.5.40": {} - '@vue/shared@3.5.41': {} + "@vue/shared@3.5.41": {} abbrev@3.0.1: {} @@ -5710,13 +8371,13 @@ snapshots: ast-kit@2.2.0: dependencies: - '@babel/parser': 7.29.7 + "@babel/parser": 7.29.7 pathe: 2.0.3 ast-walker-scope@0.9.0: dependencies: - '@babel/parser': 7.29.7 - '@babel/types': 7.29.7 + "@babel/parser": 7.29.7 + "@babel/types": 7.29.7 ast-kit: 2.2.0 async-sema@3.1.1: {} @@ -6230,32 +8891,32 @@ snapshots: esbuild@0.28.1: optionalDependencies: - '@esbuild/aix-ppc64': 0.28.1 - '@esbuild/android-arm': 0.28.1 - '@esbuild/android-arm64': 0.28.1 - '@esbuild/android-x64': 0.28.1 - '@esbuild/darwin-arm64': 0.28.1 - '@esbuild/darwin-x64': 0.28.1 - '@esbuild/freebsd-arm64': 0.28.1 - '@esbuild/freebsd-x64': 0.28.1 - '@esbuild/linux-arm': 0.28.1 - '@esbuild/linux-arm64': 0.28.1 - '@esbuild/linux-ia32': 0.28.1 - '@esbuild/linux-loong64': 0.28.1 - '@esbuild/linux-mips64el': 0.28.1 - '@esbuild/linux-ppc64': 0.28.1 - '@esbuild/linux-riscv64': 0.28.1 - '@esbuild/linux-s390x': 0.28.1 - '@esbuild/linux-x64': 0.28.1 - '@esbuild/netbsd-arm64': 0.28.1 - '@esbuild/netbsd-x64': 0.28.1 - '@esbuild/openbsd-arm64': 0.28.1 - '@esbuild/openbsd-x64': 0.28.1 - '@esbuild/openharmony-arm64': 0.28.1 - '@esbuild/sunos-x64': 0.28.1 - '@esbuild/win32-arm64': 0.28.1 - '@esbuild/win32-ia32': 0.28.1 - '@esbuild/win32-x64': 0.28.1 + "@esbuild/aix-ppc64": 0.28.1 + "@esbuild/android-arm": 0.28.1 + "@esbuild/android-arm64": 0.28.1 + "@esbuild/android-x64": 0.28.1 + "@esbuild/darwin-arm64": 0.28.1 + "@esbuild/darwin-x64": 0.28.1 + "@esbuild/freebsd-arm64": 0.28.1 + "@esbuild/freebsd-x64": 0.28.1 + "@esbuild/linux-arm": 0.28.1 + "@esbuild/linux-arm64": 0.28.1 + "@esbuild/linux-ia32": 0.28.1 + "@esbuild/linux-loong64": 0.28.1 + "@esbuild/linux-mips64el": 0.28.1 + "@esbuild/linux-ppc64": 0.28.1 + "@esbuild/linux-riscv64": 0.28.1 + "@esbuild/linux-s390x": 0.28.1 + "@esbuild/linux-x64": 0.28.1 + "@esbuild/netbsd-arm64": 0.28.1 + "@esbuild/netbsd-x64": 0.28.1 + "@esbuild/openbsd-arm64": 0.28.1 + "@esbuild/openbsd-x64": 0.28.1 + "@esbuild/openharmony-arm64": 0.28.1 + "@esbuild/sunos-x64": 0.28.1 + "@esbuild/win32-arm64": 0.28.1 + "@esbuild/win32-ia32": 0.28.1 + "@esbuild/win32-x64": 0.28.1 escalade@3.2.0: {} @@ -6267,7 +8928,7 @@ snapshots: estree-walker@3.0.3: dependencies: - '@types/estree': 1.0.9 + "@types/estree": 1.0.9 etag@1.8.1: {} @@ -6299,8 +8960,8 @@ snapshots: fast-glob@3.3.3: dependencies: - '@nodelib/fs.stat': 2.0.5 - '@nodelib/fs.walk': 1.2.8 + "@nodelib/fs.stat": 2.0.5 + "@nodelib/fs.walk": 1.2.8 glob-parent: 5.1.2 merge2: 1.4.1 micromatch: 4.0.8 @@ -6440,7 +9101,7 @@ snapshots: globby@16.2.3: dependencies: - '@sindresorhus/merge-streams': 4.0.0 + "@sindresorhus/merge-streams": 4.0.0 fast-glob: 3.3.3 ignore: 7.0.6 is-path-inside: 4.0.0 @@ -6536,14 +9197,14 @@ snapshots: impound@1.1.6(esbuild@0.28.1)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@25.5.0)(esbuild@0.28.1)(jiti@2.7.0)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.49.2)(yaml@2.9.0)): dependencies: - '@jridgewell/trace-mapping': 0.3.31 + "@jridgewell/trace-mapping": 0.3.31 es-module-lexer: 2.3.1 pathe: 2.0.3 unplugin: 3.3.0(esbuild@0.28.1)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@25.5.0)(esbuild@0.28.1)(jiti@2.7.0)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.49.2)(yaml@2.9.0)) unplugin-utils: 0.3.2 transitivePeerDependencies: - - '@farmfe/core' - - '@rspack/core' + - "@farmfe/core" + - "@rspack/core" - bun-types-no-globals - esbuild - rolldown @@ -6565,7 +9226,7 @@ snapshots: ioredis@5.11.1: dependencies: - '@ioredis/commands': 1.10.0 + "@ioredis/commands": 1.10.0 cluster-key-slot: 1.1.1 debug: 4.4.3 denque: 2.1.0 @@ -6624,7 +9285,7 @@ snapshots: is-reference@1.2.1: dependencies: - '@types/estree': 1.0.9 + "@types/estree": 1.0.9 is-regex@1.2.1: dependencies: @@ -6653,9 +9314,9 @@ snapshots: jackspeak@3.4.3: dependencies: - '@isaacs/cliui': 8.0.2 + "@isaacs/cliui": 8.0.2 optionalDependencies: - '@pkgjs/parseargs': 0.11.0 + "@pkgjs/parseargs": 0.11.0 jiti@1.21.7: {} @@ -6803,7 +9464,7 @@ snapshots: listhen@1.10.1(@parcel/watcher@2.5.6)(srvx@0.11.22): dependencies: - '@parcel/watcher-wasm': 2.6.0 + "@parcel/watcher-wasm": 2.6.0 citty: 0.2.2 consola: 3.4.2 crossws: 0.4.10(srvx@0.11.22) @@ -6820,7 +9481,7 @@ snapshots: untun: 0.2.2 uqr: 0.1.3 optionalDependencies: - '@parcel/watcher': 2.5.6 + "@parcel/watcher": 2.5.6 transitivePeerDependencies: - srvx @@ -6848,16 +9509,16 @@ snapshots: magic-string@0.30.21: dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 + "@jridgewell/sourcemap-codec": 1.5.5 magic-string@1.1.0: dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 + "@jridgewell/sourcemap-codec": 1.5.5 magicast@0.5.4: dependencies: - '@babel/parser': 7.29.8 - '@babel/types': 7.29.8 + "@babel/parser": 7.29.8 + "@babel/types": 7.29.8 source-map-js: 1.2.1 markdown-it@14.2.0: @@ -6991,7 +9652,7 @@ snapshots: micromark@4.0.2: dependencies: - '@types/debug': 4.1.12 + "@types/debug": 4.1.12 debug: 4.4.3 decode-named-character-reference: 1.2.0 devlop: 1.1.0 @@ -7087,15 +9748,15 @@ snapshots: nitropack@2.13.4(@parcel/watcher@2.5.6)(rolldown@1.2.3)(srvx@0.11.22)(vite@8.2.1(@types/node@25.5.0)(esbuild@0.28.1)(jiti@2.7.0)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.49.2)(yaml@2.9.0)): dependencies: - '@cloudflare/kv-asset-handler': 0.4.2 - '@rollup/plugin-alias': 6.0.0(rollup@4.62.4) - '@rollup/plugin-commonjs': 29.0.3(rollup@4.62.4) - '@rollup/plugin-inject': 5.0.5(rollup@4.62.4) - '@rollup/plugin-json': 6.1.0(rollup@4.62.4) - '@rollup/plugin-node-resolve': 16.0.3(rollup@4.62.4) - '@rollup/plugin-replace': 6.0.3(rollup@4.62.4) - '@rollup/plugin-terser': 1.0.0(rollup@4.62.4) - '@vercel/nft': 1.10.2(rollup@4.62.4) + "@cloudflare/kv-asset-handler": 0.4.2 + "@rollup/plugin-alias": 6.0.0(rollup@4.62.4) + "@rollup/plugin-commonjs": 29.0.3(rollup@4.62.4) + "@rollup/plugin-inject": 5.0.5(rollup@4.62.4) + "@rollup/plugin-json": 6.1.0(rollup@4.62.4) + "@rollup/plugin-node-resolve": 16.0.3(rollup@4.62.4) + "@rollup/plugin-replace": 6.0.3(rollup@4.62.4) + "@rollup/plugin-terser": 1.0.0(rollup@4.62.4) + "@vercel/nft": 1.10.2(rollup@4.62.4) archiver: 7.0.1 c12: 3.3.4(magicast@0.5.4) chokidar: 5.0.0 @@ -7158,25 +9819,25 @@ snapshots: youch: 4.1.1 youch-core: 0.3.3 transitivePeerDependencies: - - '@azure/app-configuration' - - '@azure/cosmos' - - '@azure/data-tables' - - '@azure/identity' - - '@azure/keyvault-secrets' - - '@azure/storage-blob' - - '@capacitor/preferences' - - '@deno/kv' - - '@electric-sql/pglite' - - '@farmfe/core' - - '@libsql/client' - - '@netlify/blobs' - - '@parcel/watcher' - - '@planetscale/database' - - '@rspack/core' - - '@upstash/redis' - - '@vercel/blob' - - '@vercel/functions' - - '@vercel/kv' + - "@azure/app-configuration" + - "@azure/cosmos" + - "@azure/data-tables" + - "@azure/identity" + - "@azure/keyvault-secrets" + - "@azure/storage-blob" + - "@capacitor/preferences" + - "@deno/kv" + - "@electric-sql/pglite" + - "@farmfe/core" + - "@libsql/client" + - "@netlify/blobs" + - "@parcel/watcher" + - "@planetscale/database" + - "@rspack/core" + - "@upstash/redis" + - "@vercel/blob" + - "@vercel/functions" + - "@vercel/kv" - aws4fetch - bare-abort-controller - bare-buffer @@ -7241,16 +9902,16 @@ snapshots: nuxt@4.5.1(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7))(@oxc-project/types@0.143.0)(@parcel/watcher@2.5.6)(@types/node@25.5.0)(@vue/compiler-sfc@3.5.41)(cac@6.7.14)(db0@0.3.4)(esbuild@0.28.1)(ioredis@5.11.1)(lightningcss@1.33.0)(magicast@0.5.4)(rollup-plugin-visualizer@7.0.1(rolldown@1.2.3)(rollup@4.62.4))(rollup@4.62.4)(sass-embedded@1.93.3)(sass@1.93.3)(srvx@0.11.22)(terser@5.49.2)(typescript@5.9.3)(vite@8.2.1(@types/node@25.5.0)(esbuild@0.28.1)(jiti@2.7.0)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.49.2)(yaml@2.9.0))(vue-tsc@2.2.12(typescript@5.9.3))(yaml@2.9.0): dependencies: - '@dxup/nuxt': 0.5.6(esbuild@0.28.1)(magicast@0.5.4)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@25.5.0)(esbuild@0.28.1)(jiti@2.7.0)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.49.2)(yaml@2.9.0)) - '@nuxt/cli': 3.37.0(@nuxt/schema@4.5.1)(@parcel/watcher@2.5.6)(cac@6.7.14)(magicast@0.5.4) - '@nuxt/devtools': 3.4.1(db0@0.3.4)(ioredis@5.11.1)(magic-string@1.1.0)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.28.1)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@25.5.0)(esbuild@0.28.1)(jiti@2.7.0)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.49.2)(yaml@2.9.0)))(vite@8.2.1(@types/node@25.5.0)(esbuild@0.28.1)(jiti@2.7.0)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.49.2)(yaml@2.9.0))(vue@3.5.41(typescript@5.9.3)) - '@nuxt/kit': 4.5.1(magic-string@1.1.0)(magicast@0.5.4)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.28.1)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@25.5.0)(esbuild@0.28.1)(jiti@2.7.0)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.49.2)(yaml@2.9.0))) - '@nuxt/nitro-server': 4.5.1(a53a6c67a4523b83f74d520e49e406c5) - '@nuxt/schema': 4.5.1 - '@nuxt/telemetry': 2.8.0(@nuxt/kit@4.5.1(magic-string@1.1.0)(magicast@0.5.4)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.28.1)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@25.5.0)(esbuild@0.28.1)(jiti@2.7.0)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.49.2)(yaml@2.9.0)))) - '@nuxt/vite-builder': 4.5.1(b0892b6506e328e14d8d4e8927b03cbc) - '@unhead/vue': 3.3.1(@oxc-project/types@0.143.0)(esbuild@0.28.1)(lightningcss@1.33.0)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@25.5.0)(esbuild@0.28.1)(jiti@2.7.0)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.49.2)(yaml@2.9.0))(vue@3.5.41(typescript@5.9.3)) - '@vue/shared': 3.5.41 + "@dxup/nuxt": 0.5.6(esbuild@0.28.1)(magicast@0.5.4)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@25.5.0)(esbuild@0.28.1)(jiti@2.7.0)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.49.2)(yaml@2.9.0)) + "@nuxt/cli": 3.37.0(@nuxt/schema@4.5.1)(@parcel/watcher@2.5.6)(cac@6.7.14)(magicast@0.5.4) + "@nuxt/devtools": 3.4.1(db0@0.3.4)(ioredis@5.11.1)(magic-string@1.1.0)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.28.1)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@25.5.0)(esbuild@0.28.1)(jiti@2.7.0)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.49.2)(yaml@2.9.0)))(vite@8.2.1(@types/node@25.5.0)(esbuild@0.28.1)(jiti@2.7.0)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.49.2)(yaml@2.9.0))(vue@3.5.41(typescript@5.9.3)) + "@nuxt/kit": 4.5.1(magic-string@1.1.0)(magicast@0.5.4)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.28.1)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@25.5.0)(esbuild@0.28.1)(jiti@2.7.0)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.49.2)(yaml@2.9.0))) + "@nuxt/nitro-server": 4.5.1(a53a6c67a4523b83f74d520e49e406c5) + "@nuxt/schema": 4.5.1 + "@nuxt/telemetry": 2.8.0(@nuxt/kit@4.5.1(magic-string@1.1.0)(magicast@0.5.4)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.28.1)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@25.5.0)(esbuild@0.28.1)(jiti@2.7.0)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.49.2)(yaml@2.9.0)))) + "@nuxt/vite-builder": 4.5.1(b0892b6506e328e14d8d4e8927b03cbc) + "@unhead/vue": 3.3.1(@oxc-project/types@0.143.0)(esbuild@0.28.1)(lightningcss@1.33.0)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@25.5.0)(esbuild@0.28.1)(jiti@2.7.0)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.49.2)(yaml@2.9.0))(vue@3.5.41(typescript@5.9.3)) + "@vue/shared": 3.5.41 chokidar: 5.0.0 compatx: 0.2.0 consola: 3.4.2 @@ -7301,38 +9962,38 @@ snapshots: vue: 3.5.41(typescript@5.9.3) vue-router: 5.2.0(@vue/compiler-sfc@3.5.41)(esbuild@0.28.1)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@25.5.0)(esbuild@0.28.1)(jiti@2.7.0)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.49.2)(yaml@2.9.0))(vue@3.5.41(typescript@5.9.3)) optionalDependencies: - '@parcel/watcher': 2.5.6 - '@types/node': 25.5.0 + "@parcel/watcher": 2.5.6 + "@types/node": 25.5.0 transitivePeerDependencies: - - '@azure/app-configuration' - - '@azure/cosmos' - - '@azure/data-tables' - - '@azure/identity' - - '@azure/keyvault-secrets' - - '@azure/storage-blob' - - '@babel/plugin-proposal-decorators' - - '@babel/plugin-syntax-jsx' - - '@babel/plugin-syntax-typescript' - - '@biomejs/biome' - - '@capacitor/preferences' - - '@deno/kv' - - '@electric-sql/pglite' - - '@farmfe/core' - - '@libsql/client' - - '@netlify/blobs' - - '@oxc-project/types' - - '@pinia/colada' - - '@planetscale/database' - - '@rollup/plugin-babel' - - '@rspack/core' - - '@unhead/cli' - - '@upstash/redis' - - '@vercel/blob' - - '@vercel/functions' - - '@vercel/kv' - - '@vitejs/devtools' - - '@vitejs/devtools-kit' - - '@vue/compiler-sfc' + - "@azure/app-configuration" + - "@azure/cosmos" + - "@azure/data-tables" + - "@azure/identity" + - "@azure/keyvault-secrets" + - "@azure/storage-blob" + - "@babel/plugin-proposal-decorators" + - "@babel/plugin-syntax-jsx" + - "@babel/plugin-syntax-typescript" + - "@biomejs/biome" + - "@capacitor/preferences" + - "@deno/kv" + - "@electric-sql/pglite" + - "@farmfe/core" + - "@libsql/client" + - "@netlify/blobs" + - "@oxc-project/types" + - "@pinia/colada" + - "@planetscale/database" + - "@rollup/plugin-babel" + - "@rspack/core" + - "@unhead/cli" + - "@upstash/redis" + - "@vercel/blob" + - "@vercel/functions" + - "@vercel/kv" + - "@vitejs/devtools" + - "@vitejs/devtools-kit" + - "@vue/compiler-sfc" - aws4fetch - bare-abort-controller - bare-buffer @@ -7438,7 +10099,7 @@ snapshots: oxc-walker@1.1.1(@oxc-project/types@0.143.0)(rolldown@1.2.3): optionalDependencies: - '@oxc-project/types': 0.143.0 + "@oxc-project/types": 0.143.0 rolldown: 1.2.3 package-json-from-dist@1.0.1: {} @@ -7508,7 +10169,7 @@ snapshots: postcss-colormin@8.0.2(postcss@8.5.26): dependencies: - '@colordx/core': 5.5.0 + "@colordx/core": 5.5.0 browserslist: 4.28.7 caniuse-api: 4.0.0 postcss: 8.5.26 @@ -7578,7 +10239,7 @@ snapshots: postcss-minify-gradients@8.0.2(postcss@8.5.26): dependencies: - '@colordx/core': 5.5.0 + "@colordx/core": 5.5.0 cssnano-utils: 6.0.2(postcss@8.5.26) postcss: 8.5.26 postcss-value-parser: 4.2.0 @@ -7605,8 +10266,8 @@ snapshots: postcss-nesting@13.0.2(postcss@8.5.24): dependencies: - '@csstools/selector-resolve-nested': 3.1.0(postcss-selector-parser@7.1.4) - '@csstools/selector-specificity': 5.0.0(postcss-selector-parser@7.1.4) + "@csstools/selector-resolve-nested": 3.1.0(postcss-selector-parser@7.1.4) + "@csstools/selector-specificity": 5.0.0(postcss-selector-parser@7.1.4) postcss: 8.5.24 postcss-selector-parser: 7.1.4 @@ -7826,23 +10487,23 @@ snapshots: rolldown@1.2.3: dependencies: - '@oxc-project/types': 0.143.0 - '@rolldown/pluginutils': 1.0.1 + "@oxc-project/types": 0.143.0 + "@rolldown/pluginutils": 1.0.1 optionalDependencies: - '@rolldown/binding-android-arm64': 1.2.3 - '@rolldown/binding-darwin-arm64': 1.2.3 - '@rolldown/binding-darwin-x64': 1.2.3 - '@rolldown/binding-freebsd-x64': 1.2.3 - '@rolldown/binding-linux-arm-gnueabihf': 1.2.3 - '@rolldown/binding-linux-arm64-gnu': 1.2.3 - '@rolldown/binding-linux-arm64-musl': 1.2.3 - '@rolldown/binding-linux-ppc64-gnu': 1.2.3 - '@rolldown/binding-linux-s390x-gnu': 1.2.3 - '@rolldown/binding-linux-x64-gnu': 1.2.3 - '@rolldown/binding-linux-x64-musl': 1.2.3 - '@rolldown/binding-openharmony-arm64': 1.2.3 - '@rolldown/binding-win32-arm64-msvc': 1.2.3 - '@rolldown/binding-win32-x64-msvc': 1.2.3 + "@rolldown/binding-android-arm64": 1.2.3 + "@rolldown/binding-darwin-arm64": 1.2.3 + "@rolldown/binding-darwin-x64": 1.2.3 + "@rolldown/binding-freebsd-x64": 1.2.3 + "@rolldown/binding-linux-arm-gnueabihf": 1.2.3 + "@rolldown/binding-linux-arm64-gnu": 1.2.3 + "@rolldown/binding-linux-arm64-musl": 1.2.3 + "@rolldown/binding-linux-ppc64-gnu": 1.2.3 + "@rolldown/binding-linux-s390x-gnu": 1.2.3 + "@rolldown/binding-linux-x64-gnu": 1.2.3 + "@rolldown/binding-linux-x64-musl": 1.2.3 + "@rolldown/binding-openharmony-arm64": 1.2.3 + "@rolldown/binding-win32-arm64-msvc": 1.2.3 + "@rolldown/binding-win32-x64-msvc": 1.2.3 rollup-plugin-visualizer@7.0.1(rolldown@1.2.3)(rollup@4.62.4): dependencies: @@ -7856,34 +10517,34 @@ snapshots: rollup@4.62.4: dependencies: - '@types/estree': 1.0.9 + "@types/estree": 1.0.9 optionalDependencies: - '@napi-rs/lzma-linux-x64-gnu': 1.5.1 - '@rollup/rollup-android-arm-eabi': 4.62.4 - '@rollup/rollup-android-arm64': 4.62.4 - '@rollup/rollup-darwin-arm64': 4.62.4 - '@rollup/rollup-darwin-x64': 4.62.4 - '@rollup/rollup-freebsd-arm64': 4.62.4 - '@rollup/rollup-freebsd-x64': 4.62.4 - '@rollup/rollup-linux-arm-gnueabihf': 4.62.4 - '@rollup/rollup-linux-arm-musleabihf': 4.62.4 - '@rollup/rollup-linux-arm64-gnu': 4.62.4 - '@rollup/rollup-linux-arm64-musl': 4.62.4 - '@rollup/rollup-linux-loong64-gnu': 4.62.4 - '@rollup/rollup-linux-loong64-musl': 4.62.4 - '@rollup/rollup-linux-ppc64-gnu': 4.62.4 - '@rollup/rollup-linux-ppc64-musl': 4.62.4 - '@rollup/rollup-linux-riscv64-gnu': 4.62.4 - '@rollup/rollup-linux-riscv64-musl': 4.62.4 - '@rollup/rollup-linux-s390x-gnu': 4.62.4 - '@rollup/rollup-linux-x64-gnu': 4.62.4 - '@rollup/rollup-linux-x64-musl': 4.62.4 - '@rollup/rollup-openbsd-x64': 4.62.4 - '@rollup/rollup-openharmony-arm64': 4.62.4 - '@rollup/rollup-win32-arm64-msvc': 4.62.4 - '@rollup/rollup-win32-ia32-msvc': 4.62.4 - '@rollup/rollup-win32-x64-gnu': 4.62.4 - '@rollup/rollup-win32-x64-msvc': 4.62.4 + "@napi-rs/lzma-linux-x64-gnu": 1.5.1 + "@rollup/rollup-android-arm-eabi": 4.62.4 + "@rollup/rollup-android-arm64": 4.62.4 + "@rollup/rollup-darwin-arm64": 4.62.4 + "@rollup/rollup-darwin-x64": 4.62.4 + "@rollup/rollup-freebsd-arm64": 4.62.4 + "@rollup/rollup-freebsd-x64": 4.62.4 + "@rollup/rollup-linux-arm-gnueabihf": 4.62.4 + "@rollup/rollup-linux-arm-musleabihf": 4.62.4 + "@rollup/rollup-linux-arm64-gnu": 4.62.4 + "@rollup/rollup-linux-arm64-musl": 4.62.4 + "@rollup/rollup-linux-loong64-gnu": 4.62.4 + "@rollup/rollup-linux-loong64-musl": 4.62.4 + "@rollup/rollup-linux-ppc64-gnu": 4.62.4 + "@rollup/rollup-linux-ppc64-musl": 4.62.4 + "@rollup/rollup-linux-riscv64-gnu": 4.62.4 + "@rollup/rollup-linux-riscv64-musl": 4.62.4 + "@rollup/rollup-linux-s390x-gnu": 4.62.4 + "@rollup/rollup-linux-x64-gnu": 4.62.4 + "@rollup/rollup-linux-x64-musl": 4.62.4 + "@rollup/rollup-openbsd-x64": 4.62.4 + "@rollup/rollup-openharmony-arm64": 4.62.4 + "@rollup/rollup-win32-arm64-msvc": 4.62.4 + "@rollup/rollup-win32-ia32-msvc": 4.62.4 + "@rollup/rollup-win32-x64-gnu": 4.62.4 + "@rollup/rollup-win32-x64-msvc": 4.62.4 fsevents: 2.3.3 rou3@0.9.1: {} @@ -7968,7 +10629,7 @@ snapshots: sass-embedded@1.93.3: dependencies: - '@bufbuild/protobuf': 2.10.1 + "@bufbuild/protobuf": 2.10.1 buffer-builder: 0.2.0 colorjs.io: 0.5.2 immutable: 5.1.4 @@ -8002,7 +10663,7 @@ snapshots: immutable: 5.1.4 source-map-js: 1.2.1 optionalDependencies: - '@parcel/watcher': 2.5.6 + "@parcel/watcher": 2.5.6 optional: true sax@1.6.1: {} @@ -8068,17 +10729,17 @@ snapshots: simple-git@3.36.0: dependencies: - '@kwsites/file-exists': 1.1.1 - '@kwsites/promise-deferred': 1.1.1 - '@simple-git/args-pathspec': 1.0.3 - '@simple-git/argv-parser': 1.1.1 + "@kwsites/file-exists": 1.1.1 + "@kwsites/promise-deferred": 1.1.1 + "@simple-git/args-pathspec": 1.0.3 + "@simple-git/argv-parser": 1.1.1 debug: 4.4.3 transitivePeerDependencies: - supports-color sirv@3.0.2: dependencies: - '@polka/url': 1.0.0-next.29 + "@polka/url": 1.0.0-next.29 mrmime: 2.0.1 totalist: 3.0.1 @@ -8175,7 +10836,7 @@ snapshots: sucrase@3.35.0: dependencies: - '@jridgewell/gen-mapping': 0.3.13 + "@jridgewell/gen-mapping": 0.3.13 commander: 4.1.1 glob: 10.5.0 lines-and-columns: 1.2.4 @@ -8215,7 +10876,7 @@ snapshots: tailwind-config-viewer@2.0.4(tailwindcss@3.4.18(yaml@2.9.0)): dependencies: - '@koa/router': 12.0.2 + "@koa/router": 12.0.2 commander: 6.2.1 fs-extra: 9.1.0 koa: 2.16.3 @@ -8229,7 +10890,7 @@ snapshots: tailwindcss@3.4.18(yaml@2.9.0): dependencies: - '@alloc/quick-lru': 5.2.0 + "@alloc/quick-lru": 5.2.0 arg: 5.0.2 chokidar: 3.6.0 didyoumean: 1.2.2 @@ -8268,7 +10929,7 @@ snapshots: tar@7.5.22: dependencies: - '@isaacs/fs-minipass': 4.0.1 + "@isaacs/fs-minipass": 4.0.1 chownr: 3.0.0 minipass: 7.1.3 minizlib: 3.1.0 @@ -8283,7 +10944,7 @@ snapshots: terser@5.49.2: dependencies: - '@jridgewell/source-map': 0.3.11 + "@jridgewell/source-map": 0.3.11 acorn: 8.18.0 commander: 2.20.3 source-map-support: 0.5.21 @@ -8376,8 +11037,8 @@ snapshots: optionalDependencies: vite: 8.2.1(@types/node@25.5.0)(esbuild@0.28.1)(jiti@2.7.0)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.49.2)(yaml@2.9.0) transitivePeerDependencies: - - '@farmfe/core' - - '@rspack/core' + - "@farmfe/core" + - "@rspack/core" - bun-types-no-globals - esbuild - rolldown @@ -8408,8 +11069,8 @@ snapshots: optionalDependencies: rolldown: 1.2.3 transitivePeerDependencies: - - '@farmfe/core' - - '@rspack/core' + - "@farmfe/core" + - "@rspack/core" - bun-types-no-globals - esbuild - rollup @@ -8426,14 +11087,14 @@ snapshots: unplugin@2.3.11: dependencies: - '@jridgewell/remapping': 2.3.5 + "@jridgewell/remapping": 2.3.5 acorn: 8.18.0 picomatch: 4.0.5 webpack-virtual-modules: 0.6.2 unplugin@3.3.0(esbuild@0.28.1)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@25.5.0)(esbuild@0.28.1)(jiti@2.7.0)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.49.2)(yaml@2.9.0)): dependencies: - '@jridgewell/remapping': 2.3.5 + "@jridgewell/remapping": 2.3.5 picomatch: 4.0.5 webpack-virtual-modules: 0.6.2 optionalDependencies: @@ -8520,8 +11181,8 @@ snapshots: pathe: 2.0.3 vite: 8.2.1(@types/node@25.5.0)(esbuild@0.28.1)(jiti@2.7.0)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.49.2)(yaml@2.9.0) transitivePeerDependencies: - - '@types/node' - - '@vitejs/devtools' + - "@types/node" + - "@vitejs/devtools" - esbuild - jiti - less @@ -8535,7 +11196,7 @@ snapshots: vite-plugin-checker@0.14.5(typescript@5.9.3)(vite@8.2.1(@types/node@25.5.0)(esbuild@0.28.1)(jiti@2.7.0)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.49.2)(yaml@2.9.0))(vue-tsc@2.2.12(typescript@5.9.3)): dependencies: - '@babel/code-frame': 7.29.7 + "@babel/code-frame": 7.29.7 chokidar: 5.0.0 npm-run-path: 6.0.0 picocolors: 1.1.1 @@ -8560,7 +11221,7 @@ snapshots: vite: 8.2.1(@types/node@25.5.0)(esbuild@0.28.1)(jiti@2.7.0)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.49.2)(yaml@2.9.0) vite-dev-rpc: 2.0.0(vite@8.2.1(@types/node@25.5.0)(esbuild@0.28.1)(jiti@2.7.0)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.49.2)(yaml@2.9.0)) optionalDependencies: - '@nuxt/kit': 4.5.1(magic-string@1.1.0)(magicast@0.5.4)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.28.1)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@25.5.0)(esbuild@0.28.1)(jiti@2.7.0)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.49.2)(yaml@2.9.0))) + "@nuxt/kit": 4.5.1(magic-string@1.1.0)(magicast@0.5.4)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.28.1)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@25.5.0)(esbuild@0.28.1)(jiti@2.7.0)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.49.2)(yaml@2.9.0))) vite-plugin-vue-tracer@1.4.0(vite@8.2.1(@types/node@25.5.0)(esbuild@0.28.1)(jiti@2.7.0)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.49.2)(yaml@2.9.0))(vue@3.5.41(typescript@5.9.3)): dependencies: @@ -8580,7 +11241,7 @@ snapshots: rolldown: 1.2.3 tinyglobby: 0.2.17 optionalDependencies: - '@types/node': 25.5.0 + "@types/node": 25.5.0 esbuild: 0.28.1 fsevents: 2.3.3 jiti: 2.7.0 @@ -8599,9 +11260,9 @@ snapshots: vue-router@5.2.0(@vue/compiler-sfc@3.5.41)(esbuild@0.28.1)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@25.5.0)(esbuild@0.28.1)(jiti@2.7.0)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.49.2)(yaml@2.9.0))(vue@3.5.41(typescript@5.9.3)): dependencies: - '@babel/generator': 8.0.0 - '@vue-macros/common': 3.1.4(vue@3.5.41(typescript@5.9.3)) - '@vue/devtools-api': 8.2.1 + "@babel/generator": 8.0.0 + "@vue-macros/common": 3.1.4(vue@3.5.41(typescript@5.9.3)) + "@vue/devtools-api": 8.2.1 ast-walker-scope: 0.9.0 chokidar: 5.0.0 json5: 2.2.3 @@ -8619,11 +11280,11 @@ snapshots: vue: 3.5.41(typescript@5.9.3) yaml: 2.9.0 optionalDependencies: - '@vue/compiler-sfc': 3.5.41 + "@vue/compiler-sfc": 3.5.41 vite: 8.2.1(@types/node@25.5.0)(esbuild@0.28.1)(jiti@2.7.0)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.49.2)(yaml@2.9.0) transitivePeerDependencies: - - '@farmfe/core' - - '@rspack/core' + - "@farmfe/core" + - "@rspack/core" - bun-types-no-globals - esbuild - rolldown @@ -8633,17 +11294,17 @@ snapshots: vue-tsc@2.2.12(typescript@5.9.3): dependencies: - '@volar/typescript': 2.4.15 - '@vue/language-core': 2.2.12(typescript@5.9.3) + "@volar/typescript": 2.4.15 + "@vue/language-core": 2.2.12(typescript@5.9.3) typescript: 5.9.3 vue@3.5.41(typescript@5.9.3): dependencies: - '@vue/compiler-dom': 3.5.41 - '@vue/compiler-sfc': 3.5.41 - '@vue/runtime-dom': 3.5.41 - '@vue/server-renderer': 3.5.41 - '@vue/shared': 3.5.41 + "@vue/compiler-dom": 3.5.41 + "@vue/compiler-sfc": 3.5.41 + "@vue/runtime-dom": 3.5.41 + "@vue/server-renderer": 3.5.41 + "@vue/shared": 3.5.41 optionalDependencies: typescript: 5.9.3 @@ -8731,14 +11392,14 @@ snapshots: youch-core@0.3.3: dependencies: - '@poppinss/exception': 1.2.3 + "@poppinss/exception": 1.2.3 error-stack-parser-es: 1.0.5 youch@4.1.1: dependencies: - '@poppinss/colors': 4.1.6 - '@poppinss/dumper': 0.7.0 - '@speed-highlight/core': 1.2.23 + "@poppinss/colors": 4.1.6 + "@poppinss/dumper": 0.7.0 + "@speed-highlight/core": 1.2.23 cookie-es: 3.1.1 youch-core: 0.3.3 diff --git a/desktop/main/pnpm-workspace.yaml b/desktop/main/pnpm-workspace.yaml index 02c446f78..8d4e06305 100644 --- a/desktop/main/pnpm-workspace.yaml +++ b/desktop/main/pnpm-workspace.yaml @@ -1,17 +1,17 @@ onlyBuiltDependencies: - - '@parcel/watcher' + - "@parcel/watcher" - esbuild overrides: - devalue@>=5.1.0 <5.6.2: '>=5.6.2' - devalue@>=5.3.0 <=5.6.1: '>=5.6.2' - diff@>=6.0.0 <8.0.3: '>=8.0.3' - h3@<=1.15.4: '>=1.15.5' - lodash@>=4.0.0 <=4.17.22: '>=4.17.23' - markdown-it@>=13.0.0 <14.1.1: '>=14.1.1' - node-forge@<1.3.2: '>=1.3.2' - seroval@<1.4.1: '>=1.4.1' - seroval@<=1.4.0: '>=1.4.1' - tar@<7.5.7: '>=7.5.7' - tar@<=7.5.2: '>=7.5.3' - tar@<=7.5.3: '>=7.5.4' + devalue@>=5.1.0 <5.6.2: ">=5.6.2" + devalue@>=5.3.0 <=5.6.1: ">=5.6.2" + diff@>=6.0.0 <8.0.3: ">=8.0.3" + h3@<=1.15.4: ">=1.15.5" + lodash@>=4.0.0 <=4.17.22: ">=4.17.23" + markdown-it@>=13.0.0 <14.1.1: ">=14.1.1" + node-forge@<1.3.2: ">=1.3.2" + seroval@<1.4.1: ">=1.4.1" + seroval@<=1.4.0: ">=1.4.1" + tar@<7.5.7: ">=7.5.7" + tar@<=7.5.2: ">=7.5.3" + tar@<=7.5.3: ">=7.5.4" diff --git a/desktop/main/tailwind.config.js b/desktop/main/tailwind.config.js index 2e71f1d70..053dfcdb9 100644 --- a/desktop/main/tailwind.config.js +++ b/desktop/main/tailwind.config.js @@ -1,6 +1,6 @@ -const path = require('path'); +const path = require("path"); -const dropbase = path.join(__dirname, "../../libraries/base") +const dropbase = path.join(__dirname, "../../libraries/base"); /** @type {import('tailwindcss').Config} */ export default { diff --git a/desktop/main/tsconfig.json b/desktop/main/tsconfig.json index 04227124e..854a8dacb 100644 --- a/desktop/main/tsconfig.json +++ b/desktop/main/tsconfig.json @@ -1,5 +1,5 @@ { + "exclude": ["src-tauri/**/*"], // https://nuxt.com/docs/guide/concepts/typescript - "extends": "./.nuxt/tsconfig.json", - "exclude": ["src-tauri/**/*"] + "extends": "./.nuxt/tsconfig.json" } diff --git a/desktop/main/types.ts b/desktop/main/types.ts index a42238fef..1b3b64026 100644 --- a/desktop/main/types.ts +++ b/desktop/main/types.ts @@ -54,7 +54,7 @@ export type GameVersion = { launchTemplate: string; overrideProtonPath: string; overrideHandler: string | undefined; - enableUpdates: boolean + enableUpdates: boolean; }; setups: Array<{ platform: string }>; launches: Array<{ platform: string }>; diff --git a/desktop/src-tauri/capabilities/default.json b/desktop/src-tauri/capabilities/default.json index ac70625c4..49a7edbf6 100644 --- a/desktop/src-tauri/capabilities/default.json +++ b/desktop/src-tauri/capabilities/default.json @@ -1,10 +1,7 @@ { "$schema": "../gen/schemas/desktop-schema.json", - "identifier": "default", "description": "Capability for the main window", - "windows": [ - "main" - ], + "identifier": "default", "permissions": [ "core:default", "shell:allow-open", @@ -16,5 +13,6 @@ "dialog:default", "os:default", "opener:default" - ] -} \ No newline at end of file + ], + "windows": ["main"] +} diff --git a/desktop/src-tauri/tauri.conf.json b/desktop/src-tauri/tauri.conf.json index bb1113007..b8c022339 100644 --- a/desktop/src-tauri/tauri.conf.json +++ b/desktop/src-tauri/tauri.conf.json @@ -1,32 +1,30 @@ { "$schema": "https://schema.tauri.app/config/2.0.0", - "productName": "Drop Desktop Client", - "version": "0.4.0", - "identifier": "org.droposs.client", - "build": { - "beforeDevCommand": "pnpm run -C main dev --port 1432", - "devUrl": "http://localhost:1432/", - "beforeBuildCommand": "pnpm build", - "frontendDist": "../.output" - }, "app": { "security": { - "csp": "", "assetProtocol": { "enable": true, "scope": {} - } + }, + "csp": "" } }, - "plugins": { - "deep-link": { - "desktop": { - "schemes": ["drop"] - } - } + "build": { + "beforeBuildCommand": "pnpm build", + "beforeDevCommand": "pnpm run -C main dev --port 1432", + "devUrl": "http://localhost:1432/", + "frontendDist": "../.output" }, "bundle": { "active": true, + "externalBin": [], + "icon": [ + "icons/32x32.png", + "icons/128x128.png", + "icons/128x128@2x.png", + "icons/icon.icns", + "icons/icon.ico" + ], "targets": ["nsis", "deb", "rpm", "dmg"], "windows": { "nsis": { @@ -37,14 +35,16 @@ "type": "embedBootstrapper" }, "wix": null - }, - "icon": [ - "icons/32x32.png", - "icons/128x128.png", - "icons/128x128@2x.png", - "icons/icon.icns", - "icons/icon.ico" - ], - "externalBin": [] - } + } + }, + "identifier": "org.droposs.client", + "plugins": { + "deep-link": { + "desktop": { + "schemes": ["drop"] + } + } + }, + "productName": "Drop Desktop Client", + "version": "0.4.0" } diff --git a/libraries/base/components/LoadingButton.vue b/libraries/base/components/LoadingButton.vue index 9a2e83e17..982473bbb 100644 --- a/libraries/base/components/LoadingButton.vue +++ b/libraries/base/components/LoadingButton.vue @@ -46,8 +46,7 @@ const styles: { ["default"]: { base: "inline-flex min-h-9 items-center justify-center rounded-md px-3 py-1.5 text-sm font-semibold leading-6 shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600", dft: "text-white bg-blue-600 hover:bg-blue-500", - disabled: - "text-zinc-400 bg-blue-600/10 hover:bg-blue-600/10", + disabled: "text-zinc-400 bg-blue-600/10 hover:bg-blue-600/10", }, ["none"]: { base: "", dft: "", disabled: "" }, }; diff --git a/libraries/base/components/ModalTemplate.vue b/libraries/base/components/ModalTemplate.vue index 14f5a14f0..325ab81b4 100644 --- a/libraries/base/components/ModalTemplate.vue +++ b/libraries/base/components/ModalTemplate.vue @@ -10,9 +10,7 @@ leave-from="opacity-100" leave-to="opacity-0" > -
    +
    @@ -29,7 +27,10 @@ leave-to="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" >
    @@ -56,7 +57,7 @@ import { } from "@headlessui/vue"; const open: Ref = defineModel() as any; -const props = defineProps<{sizeClass?: string}>(); +const props = defineProps<{ sizeClass?: string }>(); function close() { open.value = false; diff --git a/libraries/base/components/NotificationModal.vue b/libraries/base/components/NotificationModal.vue index d6a46f493..6c827e6c3 100644 --- a/libraries/base/components/NotificationModal.vue +++ b/libraries/base/components/NotificationModal.vue @@ -1,47 +1,44 @@ diff --git a/server/components/GameSearchResultWidget.vue b/server/components/GameSearchResultWidget.vue index d4afe6777..6a8ce6613 100644 --- a/server/components/GameSearchResultWidget.vue +++ b/server/components/GameSearchResultWidget.vue @@ -3,6 +3,8 @@

    diff --git a/server/components/ImportVersionLaunchRow.vue b/server/components/ImportVersionLaunchRow.vue index a59a66634..40095593c 100644 --- a/server/components/ImportVersionLaunchRow.vue +++ b/server/components/ImportVersionLaunchRow.vue @@ -9,6 +9,7 @@ v-model="launchConfiguration.name" type="text" name="startup" + aria-label="Launch name" class="block flex-1 border-0 py-1.5 px-3 bg-transparent text-zinc-100 placeholder:text-zinc-400 focus:ring-0 sm:text-sm sm:leading-6" placeholder="Launch name" /> @@ -90,6 +91,8 @@ v-if="guess.type === 'emulator'" :src="useObject(guess.icon)" class="size-5" + alt="" + aria-hidden="true" /> @@ -198,6 +201,7 @@ $t("library.admin.launchRow.emulatorSelect") }}

    - +
    diff --git a/server/components/Selector/Platform.vue b/server/components/Selector/Platform.vue index 4b73c18a4..6cf052ec5 100644 --- a/server/components/Selector/Platform.vue +++ b/server/components/Selector/Platform.vue @@ -91,7 +91,10 @@ const typedModel = computed({ return model.value || null; }, set(v) { - if (v === null) return (model.value = undefined); + if (v === null) { + model.value = undefined; + return; + } model.value = v; }, }); diff --git a/server/components/SourceOptions/Filesystem.vue b/server/components/SourceOptions/Filesystem.vue index 9f3e310b6..d80d0f2a1 100644 --- a/server/components/SourceOptions/Filesystem.vue +++ b/server/components/SourceOptions/Filesystem.vue @@ -14,7 +14,7 @@ v-model="model!.baseDir" name="path" type="text" - autocomplete="path" + autocomplete="off" :placeholder="$t('library.admin.sources.fsPathPlaceholder')" class="block w-full rounded-md border-0 py-1.5 px-3 bg-zinc-800 disabled:bg-zinc-900/80 text-zinc-100 disabled:text-zinc-400 shadow-sm ring-1 ring-inset ring-zinc-700 disabled:ring-zinc-800 placeholder:text-zinc-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6" /> diff --git a/server/components/SourceOptions/FlatFilesystem.vue b/server/components/SourceOptions/FlatFilesystem.vue index 9f3e310b6..d80d0f2a1 100644 --- a/server/components/SourceOptions/FlatFilesystem.vue +++ b/server/components/SourceOptions/FlatFilesystem.vue @@ -14,7 +14,7 @@ v-model="model!.baseDir" name="path" type="text" - autocomplete="path" + autocomplete="off" :placeholder="$t('library.admin.sources.fsPathPlaceholder')" class="block w-full rounded-md border-0 py-1.5 px-3 bg-zinc-800 disabled:bg-zinc-900/80 text-zinc-100 disabled:text-zinc-400 shadow-sm ring-1 ring-inset ring-zinc-700 disabled:ring-zinc-800 placeholder:text-zinc-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6" /> diff --git a/server/components/SourceTable.vue b/server/components/SourceTable.vue index 584672f01..ddf073b79 100644 --- a/server/components/SourceTable.vue +++ b/server/components/SourceTable.vue @@ -114,6 +114,7 @@ >
    -
    -
    - - + />
diff --git a/server/pages/admin/library/[id]/import.vue b/server/pages/admin/library/[id]/import.vue index 53ab061c2..6c9863f8c 100644 --- a/server/pages/admin/library/[id]/import.vue +++ b/server/pages/admin/library/[id]/import.vue @@ -77,9 +77,11 @@
- +

{{ $t("library.admin.import.version.displayNameDesc") }}

@@ -99,9 +101,9 @@
- +

+ {{ $t("library.admin.import.version.setupCmd") }} +

{{ $t("library.admin.import.version.setupDesc") }}

@@ -121,6 +123,7 @@ :needs-name="false" />
+
diff --git a/server/pages/admin/library/[id]/index.vue b/server/pages/admin/library/[id]/index.vue index b9c56c759..c42bbf8f5 100644 --- a/server/pages/admin/library/[id]/index.vue +++ b/server/pages/admin/library/[id]/index.vue @@ -74,6 +74,7 @@ :key="value" >
-
{{ $t("library.admin.import.loading") }} @@ -284,7 +284,7 @@ fill="currentFill" /> -
+
@@ -228,6 +229,7 @@ v-slot="{ active }" >
- +

+ {{ $t("type") }} +

{{ $t("library.admin.sources.typeDesc") }}

diff --git a/server/pages/admin/metadata/companies/[id]/index.vue b/server/pages/admin/metadata/companies/[id]/index.vue index 7f0841057..aa50237d5 100644 --- a/server/pages/admin/metadata/companies/[id]/index.vue +++ b/server/pages/admin/metadata/companies/[id]/index.vue @@ -6,6 +6,8 @@
@@ -22,7 +24,7 @@ class="group/name inline-flex items-center gap-x-3 text-5xl font-bold font-display text-zinc-100" > {{ company.mName }} -
-
@@ -65,7 +67,7 @@

Download latest server snapshot and overwrite local files.

-
+
From 885bc3d12250597ce516ed05047282cc3cfae419 Mon Sep 17 00:00:00 2001 From: John Smith Date: Mon, 14 Sep 2026 01:12:05 -0400 Subject: [PATCH 166/243] feat(plugins): add CloudSavePathResolver SPI to core server and desktop client - Add the cloudsave:provider capability and registerCloudSaveResolver to the server PluginContext and desktop ClientPluginContext, mirroring the existing MetadataProvider/PaymentGateway SPIs (collision guards, capability gating, unregister cleanup, and inspection getters). - Add CloudSavePattern/GameInstallContext/CloudSavePathResolver contracts to both core type modules. - Cover registration, capability denial, collision, and teardown in the server plugins and desktop client plugin-manager test suites. --- .../internal/plugins/ClientPluginManager.ts | 36 +++++++++ .../__tests__/client_plugin_manager.test.ts | 20 ++++- desktop/main/internal/plugins/types.ts | 32 ++++++++ .../plugins/__tests__/plugins.test.ts | 79 ++++++++++++++++++- .../__tests__/upstream_compatibility.test.ts | 5 ++ server/server/internal/plugins/manager.ts | 71 +++++++++++++---- server/server/internal/plugins/types.ts | 33 ++++++++ 7 files changed, 259 insertions(+), 17 deletions(-) diff --git a/desktop/main/internal/plugins/ClientPluginManager.ts b/desktop/main/internal/plugins/ClientPluginManager.ts index fe9c62683..0ccbe9923 100644 --- a/desktop/main/internal/plugins/ClientPluginManager.ts +++ b/desktop/main/internal/plugins/ClientPluginManager.ts @@ -6,6 +6,7 @@ import type { ClientPluginContext, ClientPluginStorage, ClientPluginWebSocket, + CloudSavePathResolver, CommandResult, GameMenuItem, HttpMethod, @@ -214,6 +215,9 @@ export class ClientPluginManager { public readonly metadataProviders = reactive< Array<{ pluginId: string; provider: MetadataProvider }> >([]); + public readonly cloudSaveResolvers = reactive< + Array<{ pluginId: string; resolver: CloudSavePathResolver }> + >([]); public readonly isInitialized = ref(false); @@ -348,6 +352,29 @@ export class ClientPluginManager { if (idx !== -1) this.metadataProviders.splice(idx, 1); }; }, + registerCloudSaveResolver: (resolver: CloudSavePathResolver) => { + if ( + capabilities.length > 0 && + !capabilities.includes("cloudsave:provider") + ) { + throw new Error( + `Client plugin '${pluginId}' attempted 'registerCloudSaveResolver' without the 'cloudsave:provider' capability`, + ); + } + if ( + !resolver || + typeof resolver.id !== "string" || + !resolver.id.trim() + ) { + throw new Error("Cloud save resolver must have a valid non-empty id"); + } + const entry = { pluginId, resolver }; + this.cloudSaveResolvers.push(entry); + return () => { + const idx = this.cloudSaveResolvers.indexOf(entry); + if (idx !== -1) this.cloudSaveResolvers.splice(idx, 1); + }; + }, gameFs: new TauriScopedGameFs(), gameScanner: new TauriScopedGameScanner(), serverWs: new TauriPluginWebSocket(), @@ -430,6 +457,11 @@ export class ClientPluginManager { this.metadataProviders.splice(i, 1); } } + for (let i = this.cloudSaveResolvers.length - 1; i >= 0; i--) { + if (this.cloudSaveResolvers[i].pluginId === pluginId) { + this.cloudSaveResolvers.splice(i, 1); + } + } } getStoreScanners(): StoreScanner[] { @@ -440,6 +472,10 @@ export class ClientPluginManager { return this.metadataProviders.map((e) => e.provider); } + getCloudSaveResolvers(): CloudSavePathResolver[] { + return this.cloudSaveResolvers.map((e) => e.resolver); + } + /** * Load client plugin bundle from a URL (e.g. served by Drop server). */ diff --git a/desktop/main/internal/plugins/__tests__/client_plugin_manager.test.ts b/desktop/main/internal/plugins/__tests__/client_plugin_manager.test.ts index 35e31b1a3..0eb20cdc2 100644 --- a/desktop/main/internal/plugins/__tests__/client_plugin_manager.test.ts +++ b/desktop/main/internal/plugins/__tests__/client_plugin_manager.test.ts @@ -4,6 +4,7 @@ import { ClientPluginManager } from "../ClientPluginManager"; import type { ClientPlugin, ClientPluginContext, + CloudSavePathResolver, MetadataProvider, StoreScanner, } from "../types"; @@ -72,6 +73,15 @@ test("ClientPluginManager registers StoreScanner and MetadataProvider SPIs", asy let unregisterScanner: (() => void) | undefined; let unregisterProvider: (() => void) | undefined; + let unregisterCloudSave: (() => void) | undefined; + + const ludusaviResolver: CloudSavePathResolver = { + id: "ludusavi", + name: "Ludusavi", + resolveSavePaths: async (gameContext) => [ + { pattern: `%APPDATA%/${gameContext.gameTitle}` }, + ], + }; const aggregatorPlugin: ClientPlugin = { metadata: { @@ -86,6 +96,7 @@ test("ClientPluginManager registers StoreScanner and MetadataProvider SPIs", asy screenscraperProvider, ); } + unregisterCloudSave = ctx.registerCloudSaveResolver?.(ludusaviResolver); }, }; @@ -93,7 +104,7 @@ test("ClientPluginManager registers StoreScanner and MetadataProvider SPIs", asy aggregatorPlugin, "aggregator", [], - ["client:library-scan", "metadata:provider"], + ["client:library-scan", "metadata:provider", "cloudsave:provider"], ); // Assert registered @@ -108,6 +119,10 @@ test("ClientPluginManager registers StoreScanner and MetadataProvider SPIs", asy assert.equal(providers.length, 1); assert.equal(providers[0].id, "screenscraper"); + const resolvers = manager.getCloudSaveResolvers(); + assert.equal(resolvers.length, 1); + assert.equal(resolvers[0].id, "ludusavi"); + // Capability enforcement const restrictedPlugin: ClientPlugin = { metadata: { @@ -140,6 +155,9 @@ test("ClientPluginManager registers StoreScanner and MetadataProvider SPIs", asy unregisterProvider?.(); assert.equal(manager.getMetadataProviders().length, 0); + + unregisterCloudSave?.(); + assert.equal(manager.getCloudSaveResolvers().length, 0); }); test("ClientPluginManager cleans up SPI entries and slots on unregisterPlugin", async () => { diff --git a/desktop/main/internal/plugins/types.ts b/desktop/main/internal/plugins/types.ts index 841142d9a..bcb777294 100644 --- a/desktop/main/internal/plugins/types.ts +++ b/desktop/main/internal/plugins/types.ts @@ -22,6 +22,7 @@ export type ClientCapability = | "system:sidecar" | "system:command" | "metadata:provider" + | "cloudsave:provider" | "client:library-scan"; export type HttpMethod = "GET" | "POST" | "PUT" | "DELETE" | "PATCH" | "ALL"; @@ -189,6 +190,11 @@ export interface ClientPluginContext { * Requires the `metadata:provider` capability. */ registerMetadataProvider?(provider: MetadataProvider): () => void; + /** + * Register a client-side cloud save path resolver SPI implementation. + * Requires the `cloudsave:provider` capability. + */ + registerCloudSaveResolver?(resolver: CloudSavePathResolver): () => void; gameFs: ScopedGameFs; gameScanner: ScopedGameScanner; serverWs: ClientPluginWebSocket; @@ -270,3 +276,29 @@ export interface StoreScanner { scan(): Promise; launch?(externalId: string): Promise; } + +// ========================================== +// Cloud Save Provider SPI (#9) +// ========================================== + +export interface CloudSavePattern { + pattern: string; + platform?: "windows" | "linux" | "macos"; + winePrefix?: boolean; +} + +export interface GameInstallContext { + gameId: string; + gameTitle: string; + installDir?: string; + winePrefix?: string; + executableName?: string; +} + +export interface CloudSavePathResolver { + id: string; + name: string; + resolveSavePaths( + gameContext: GameInstallContext, + ): Promise; +} diff --git a/server/server/internal/plugins/__tests__/plugins.test.ts b/server/server/internal/plugins/__tests__/plugins.test.ts index 860a33927..75d93d560 100644 --- a/server/server/internal/plugins/__tests__/plugins.test.ts +++ b/server/server/internal/plugins/__tests__/plugins.test.ts @@ -1357,7 +1357,7 @@ test("signed bundle with FilePluginStorage survives storage mutations and reload } }); -test("PluginManager registers and gates MetadataProvider and PaymentGateway SPIs", async () => { +test("PluginManager registers and gates MetadataProvider, CloudSavePathResolver, and PaymentGateway SPIs", async () => { const manager = createTestManager(); const metadataPlugin: ServerPlugin = { @@ -1412,6 +1412,34 @@ test("PluginManager registers and gates MetadataProvider and PaymentGateway SPIs await manager.registerPlugin(metadataPlugin); await manager.registerPlugin(paymentPlugin); + const cloudSavePlugin: ServerPlugin = { + metadata: { + id: "ludusavi-cloudsave", + name: "Ludusavi Cloud Saves", + version: "1.0.0", + apiVersion: PLUGIN_API_VERSION, + capabilities: ["cloudsave:provider"], + }, + init: (ctx: PluginContext) => { + ctx.registerCloudSaveResolver({ + id: "ludusavi", + name: "Ludusavi Manifest Resolver", + resolveSavePaths: async (gameContext) => [ + { pattern: `%APPDATA%/${gameContext.gameTitle}/saves` }, + ], + }); + }, + }; + await manager.registerPlugin(cloudSavePlugin); + + const resolvers = manager.getCloudSaveResolvers(); + assert.equal(resolvers.length, 1); + assert.equal(resolvers[0].id, "ludusavi"); + assert.equal( + manager.getCloudSaveResolver("ludusavi")?.name, + "Ludusavi Manifest Resolver", + ); + // Assert registered const providers = manager.getMetadataProviders(); assert.equal(providers.length, 1); @@ -1522,11 +1550,60 @@ test("PluginManager registers and gates MetadataProvider and PaymentGateway SPIs /attempted 'registerMetadataProvider\(denied\)' without the 'metadata:provider' capability/, ); + // Cloud save resolver: collision, invalid id, and capability gate + const collidingCloudSavePlugin: ServerPlugin = { + metadata: { + id: "colliding-cloudsave-plugin", + name: "Colliding Cloud Save", + version: "1.0.0", + apiVersion: PLUGIN_API_VERSION, + capabilities: ["cloudsave:provider"], + }, + init: (ctx: PluginContext) => { + ctx.registerCloudSaveResolver({ + id: "ludusavi", + name: "Fake Ludusavi", + resolveSavePaths: async () => [], + }); + }, + }; + + await assert.rejects( + () => manager.registerPlugin(collidingCloudSavePlugin), + /Cloud save resolver 'ludusavi' is already claimed by plugin 'ludusavi-cloudsave'/, + ); + + const deniedCloudSavePlugin: ServerPlugin = { + metadata: { + id: "denied-cloudsave", + name: "Denied Cloud Save", + version: "1.0.0", + apiVersion: PLUGIN_API_VERSION, + capabilities: ["routes"], + }, + init: (ctx: PluginContext) => { + ctx.registerCloudSaveResolver({ + id: "denied", + name: "Denied", + resolveSavePaths: async () => [], + }); + }, + }; + + await assert.rejects( + () => manager.registerPlugin(deniedCloudSavePlugin), + /attempted 'registerCloudSaveResolver\(denied\)' without the 'cloudsave:provider' capability/, + ); + // Unregister cleans up SPI entries await manager.unregisterPlugin("steamgriddb-provider"); assert.equal(manager.getMetadataProviders().length, 0); assert.equal(manager.getMetadataProvider("steamgriddb"), undefined); + await manager.unregisterPlugin("ludusavi-cloudsave"); + assert.equal(manager.getCloudSaveResolvers().length, 0); + assert.equal(manager.getCloudSaveResolver("ludusavi"), undefined); + await manager.unregisterPlugin("stripe-gateway"); assert.equal(manager.getPaymentGateways().length, 0); assert.equal(manager.getPaymentGateway("stripe"), undefined); diff --git a/server/server/internal/plugins/__tests__/upstream_compatibility.test.ts b/server/server/internal/plugins/__tests__/upstream_compatibility.test.ts index cfcb2528c..d198da3a7 100644 --- a/server/server/internal/plugins/__tests__/upstream_compatibility.test.ts +++ b/server/server/internal/plugins/__tests__/upstream_compatibility.test.ts @@ -50,6 +50,11 @@ test("Upstream Invariant: Zero-plugin baseline leaves server in pure vanilla sta 0, "No metadata providers should exist by default", ); + assert.equal( + manager.getCloudSaveResolvers().length, + 0, + "No cloud save resolvers should exist by default", + ); assert.equal( manager.getPaymentGateways().length, 0, diff --git a/server/server/internal/plugins/manager.ts b/server/server/internal/plugins/manager.ts index 9043bd796..0348b50ca 100644 --- a/server/server/internal/plugins/manager.ts +++ b/server/server/internal/plugins/manager.ts @@ -15,6 +15,7 @@ import { } from "./errors"; import { PluginRegistry } from "./registry"; import type { + CloudSavePathResolver, HttpMethod, MetadataProvider, PaymentGateway, @@ -167,6 +168,10 @@ export class PluginManager { string, { pluginId: string; provider: MetadataProvider } >(); + private readonly cloudSaveResolvers = new Map< + string, + { pluginId: string; resolver: CloudSavePathResolver } + >(); private readonly paymentGateways = new Map< string, { pluginId: string; gateway: PaymentGateway } @@ -421,6 +426,33 @@ export class PluginManager { this.metadataProviders.set(provider.id, { pluginId: id, provider }); pluginLogger.debug(`Registered metadata provider: ${provider.id}`); }, + registerCloudSaveResolver: (resolver: CloudSavePathResolver) => { + if ( + !resolver || + typeof resolver.id !== "string" || + !resolver.id.trim() + ) { + throw new Error("Cloud save resolver must have a valid non-empty id"); + } + if (!this.hasCapability(capabilities, "cloudsave:provider")) { + throw new PluginCapabilityError( + id, + "cloudsave:provider", + `registerCloudSaveResolver(${resolver.id})`, + ); + } + const existing = this.cloudSaveResolvers.get(resolver.id); + if (existing && existing.pluginId !== id) { + throw new Error( + `Cloud save resolver '${resolver.id}' is already claimed by plugin '${existing.pluginId}'`, + ); + } + this.cloudSaveResolvers.set(resolver.id, { + pluginId: id, + resolver, + }); + pluginLogger.debug(`Registered cloud save resolver: ${resolver.id}`); + }, registerPaymentGateway: (gateway: PaymentGateway) => { if (!gateway || typeof gateway.id !== "string" || !gateway.id.trim()) { throw new Error("Payment gateway must have a valid non-empty id"); @@ -834,29 +866,30 @@ export class PluginManager { this.log.info(`Plugin ${id} unregistered`); } - /** Drop a plugin's routes, sockets and event subscriptions. */ - private releasePluginResources(id: string): void { - this.routes.delete(id); - for (const [channel, entry] of this.webSockets) { + /** Deletes every map entry whose value is owned by plugin `id`. */ + private purgeOwned( + map: Map, + id: string, + ): void { + for (const [key, entry] of map) { if (entry.pluginId === id) { - this.webSockets.delete(channel); + map.delete(key); } } + } + + /** Drop a plugin's routes, sockets and event subscriptions. */ + private releasePluginResources(id: string): void { + this.routes.delete(id); + this.purgeOwned(this.webSockets, id); for (const [channel, pluginId] of this.publicChannels) { if (pluginId === id) { this.publicChannels.delete(channel); } } - for (const [providerId, entry] of this.metadataProviders) { - if (entry.pluginId === id) { - this.metadataProviders.delete(providerId); - } - } - for (const [gatewayId, entry] of this.paymentGateways) { - if (entry.pluginId === id) { - this.paymentGateways.delete(gatewayId); - } - } + this.purgeOwned(this.metadataProviders, id); + this.purgeOwned(this.cloudSaveResolvers, id); + this.purgeOwned(this.paymentGateways, id); this.subscriptionAuthorizers.delete(id); const subscriptions = this.pluginEventSubscriptions.get(id) ?? []; for (const off of subscriptions) { @@ -1258,6 +1291,14 @@ export class PluginManager { return this.metadataProviders.get(id)?.provider; } + getCloudSaveResolvers(): CloudSavePathResolver[] { + return Array.from(this.cloudSaveResolvers.values()).map((e) => e.resolver); + } + + getCloudSaveResolver(id: string): CloudSavePathResolver | undefined { + return this.cloudSaveResolvers.get(id)?.resolver; + } + getPaymentGateways(): PaymentGateway[] { return Array.from(this.paymentGateways.values()).map((e) => e.gateway); } diff --git a/server/server/internal/plugins/types.ts b/server/server/internal/plugins/types.ts index 6784e2b03..95af13537 100644 --- a/server/server/internal/plugins/types.ts +++ b/server/server/internal/plugins/types.ts @@ -27,6 +27,7 @@ export type ServerCapability = | "events" | "network" | "metadata:provider" + | "cloudsave:provider" | "commerce:payment"; export type ClientCapability = @@ -43,6 +44,7 @@ export type ClientCapability = | "system:sidecar" | "system:command" | "metadata:provider" + | "cloudsave:provider" | "client:library-scan"; export type PluginCapability = ServerCapability | ClientCapability; @@ -216,6 +218,11 @@ export interface PluginContext { * Requires the `metadata:provider` capability. */ registerMetadataProvider(provider: MetadataProvider): void; + /** + * Register a cloud save path resolver SPI implementation. + * Requires the `cloudsave:provider` capability. + */ + registerCloudSaveResolver(resolver: CloudSavePathResolver): void; /** * Register a payment gateway SPI implementation. * Requires the `commerce:payment` capability. @@ -268,6 +275,32 @@ export interface MetadataProvider { getDetails(id: string): Promise; } +// ========================================== +// Cloud Save Provider SPI (#9) +// ========================================== + +export interface CloudSavePattern { + pattern: string; + platform?: "windows" | "linux" | "macos"; + winePrefix?: boolean; +} + +export interface GameInstallContext { + gameId: string; + gameTitle: string; + installDir?: string; + winePrefix?: string; + executableName?: string; +} + +export interface CloudSavePathResolver { + id: string; + name: string; + resolveSavePaths( + gameContext: GameInstallContext, + ): Promise; +} + // ========================================== // Store Scanner SPI (#21) // ========================================== From aa429cc27f09069677e48086fa43f42e7144c934 Mon Sep 17 00:00:00 2001 From: John Smith Date: Mon, 14 Sep 2026 01:13:01 -0400 Subject: [PATCH 167/243] refactor(desktop): extract plugin-ownership purge helper to satisfy complexity lint --- .../internal/plugins/ClientPluginManager.ts | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/desktop/main/internal/plugins/ClientPluginManager.ts b/desktop/main/internal/plugins/ClientPluginManager.ts index 0ccbe9923..a81b4d4df 100644 --- a/desktop/main/internal/plugins/ClientPluginManager.ts +++ b/desktop/main/internal/plugins/ClientPluginManager.ts @@ -447,19 +447,19 @@ export class ClientPluginManager { } // Clean up store scanners and metadata providers registered by this plugin - for (let i = this.storeScanners.length - 1; i >= 0; i--) { - if (this.storeScanners[i].pluginId === pluginId) { - this.storeScanners.splice(i, 1); - } - } - for (let i = this.metadataProviders.length - 1; i >= 0; i--) { - if (this.metadataProviders[i].pluginId === pluginId) { - this.metadataProviders.splice(i, 1); - } - } - for (let i = this.cloudSaveResolvers.length - 1; i >= 0; i--) { - if (this.cloudSaveResolvers[i].pluginId === pluginId) { - this.cloudSaveResolvers.splice(i, 1); + this.purgeOwned(this.storeScanners, pluginId); + this.purgeOwned(this.metadataProviders, pluginId); + this.purgeOwned(this.cloudSaveResolvers, pluginId); + } + + /** Removes every reactive entry owned by `pluginId` from `entries`. */ + private purgeOwned( + entries: T[], + pluginId: string, + ): void { + for (let i = entries.length - 1; i >= 0; i--) { + if (entries[i].pluginId === pluginId) { + entries.splice(i, 1); } } } From 9a511fb114999ea84a7947f3a1b8d8270977c3fc Mon Sep 17 00:00:00 2001 From: John Smith Date: Mon, 14 Sep 2026 01:19:11 -0400 Subject: [PATCH 168/243] feat(cloud-saves): sync Ludusavi snapshots to the Drop server end-to-end - Fix a real server bug: save objects were created with no permissions, so the owner could never download them. Grant the owner read access and enforce saveSlotSizeLimit (413 + object cleanup) on push. - Add a CloudSaveTransport trait with pure, unit-tested parsing and a hybrid pre-launch conflict policy (fast-forward when clean, timestamped rollback archive when the local cache diverged). - Implement DropServerTransport in the process crate using the authenticated remote client, and route pre-launch/post-exit hooks through it when signed in. - Expose fetch_cloud_save_slots/download_cloud_save_object Tauri commands and wire SaveSlotManager.vue into the game detail page with real slot data. --- .../components/GameDetail/SaveSlotManager.vue | 103 ++++---- desktop/main/pages/library/[id]/index.vue | 3 + desktop/src-tauri/Cargo.lock | 2 + desktop/src-tauri/cloud_saves/Cargo.toml | 1 + desktop/src-tauri/cloud_saves/src/lib.rs | 7 + desktop/src-tauri/cloud_saves/src/ludusavi.rs | 113 +++++--- desktop/src-tauri/cloud_saves/src/sync.rs | 248 ++++++++++++++++++ .../src-tauri/cloud_saves/src/transport.rs | 93 +++++++ desktop/src-tauri/process/Cargo.toml | 1 + .../process/src/cloud_save_transport.rs | 103 ++++++++ desktop/src-tauri/process/src/lib.rs | 1 + .../src-tauri/process/src/process_manager.rs | 44 +++- desktop/src-tauri/src/cloud_saves.rs | 101 +++++++ desktop/src-tauri/src/lib.rs | 5 + .../internal/saves/__tests__/saves.test.ts | 35 ++- server/server/internal/saves/manager.ts | 21 +- 16 files changed, 779 insertions(+), 102 deletions(-) create mode 100644 desktop/src-tauri/cloud_saves/src/sync.rs create mode 100644 desktop/src-tauri/cloud_saves/src/transport.rs create mode 100644 desktop/src-tauri/process/src/cloud_save_transport.rs create mode 100644 desktop/src-tauri/src/cloud_saves.rs diff --git a/desktop/main/components/GameDetail/SaveSlotManager.vue b/desktop/main/components/GameDetail/SaveSlotManager.vue index b4493bd7b..d369bb25e 100644 --- a/desktop/main/components/GameDetail/SaveSlotManager.vue +++ b/desktop/main/components/GameDetail/SaveSlotManager.vue @@ -14,7 +14,7 @@ Cloud Save Synchronization

- Powered by Ludusavi discovery & snapshot versioning + Powered by Ludusavi discovery & snapshot versioning

@@ -30,13 +30,17 @@ type="button" :disabled="isSyncing" class="rounded-md bg-white/5 px-3 py-1.5 text-xs font-medium text-white hover:bg-white/10 disabled:opacity-50 transition" - @click="syncNow" + @click="refresh" > - {{ isSyncing ? "Syncing..." : "Sync Now" }} + {{ isSyncing ? "Syncing..." : "Refresh" }} +
+ {{ errorMessage }} +
+
- Size: {{ formatBytes(slot.sizeBytes) }} + {{ slot.historyCount }} snapshot(s) • {{ - slot.lastSynced - ? `Synced ${new Date(slot.lastSynced).toLocaleTimeString()}` + slot.createdAt + ? `Created ${new Date(slot.createdAt).toLocaleString()}` : "Not yet synced" }} - • Host: {{ slot.clientHostname }}• Client: {{ slot.lastUsedClientId }}
@@ -81,44 +85,39 @@
-
- No cloud saves recorded for this title. Saves will automatically be - detected and uploaded on game exit. + No cloud saves recorded for this title. Saves are detected and uploaded + automatically when the game exits.
diff --git a/desktop/main/pages/library/[id]/index.vue b/desktop/main/pages/library/[id]/index.vue index 543c58e55..7854975e4 100644 --- a/desktop/main/pages/library/[id]/index.vue +++ b/desktop/main/pages/library/[id]/index.vue @@ -124,6 +124,9 @@ + + +
diff --git a/desktop/src-tauri/Cargo.lock b/desktop/src-tauri/Cargo.lock index e87613c0c..519a29334 100644 --- a/desktop/src-tauri/Cargo.lock +++ b/desktop/src-tauri/Cargo.lock @@ -742,6 +742,7 @@ dependencies = [ name = "cloud_saves" version = "0.1.0" dependencies = [ + "chrono", "database", "dirs 6.0.0", "log", @@ -4188,6 +4189,7 @@ dependencies = [ "log", "page_size", "regex", + "remote", "serde", "serde_json", "serde_with", diff --git a/desktop/src-tauri/cloud_saves/Cargo.toml b/desktop/src-tauri/cloud_saves/Cargo.toml index dbf98e4d4..1bc0b1281 100644 --- a/desktop/src-tauri/cloud_saves/Cargo.toml +++ b/desktop/src-tauri/cloud_saves/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" edition = "2024" [dependencies] +chrono = "0.4.42" database = { version = "0.1.0", path = "../database" } dirs = "6.0.0" log = "0.4.28" diff --git a/desktop/src-tauri/cloud_saves/src/lib.rs b/desktop/src-tauri/cloud_saves/src/lib.rs index dcd369a61..d96054100 100644 --- a/desktop/src-tauri/cloud_saves/src/lib.rs +++ b/desktop/src-tauri/cloud_saves/src/lib.rs @@ -7,9 +7,16 @@ pub mod normalise; pub mod path; pub mod placeholder; pub mod resolver; +pub mod sync; +pub mod transport; pub use ludusavi::{ pack_save_archive, sync_post_exit, sync_pre_launch, unpack_save_archive, CloudSaveSyncContext, CloudSaveSyncManager, LudusaviBackupResponse, LudusaviClient, LudusaviFileResult, LudusaviGameResult, LudusaviOverall, }; +pub use sync::{ + decide_pre_launch, sync_post_exit_with, sync_pre_launch_with, PreLaunchDecision, SyncAction, + DEFAULT_SLOT, +}; +pub use transport::{parse_latest_snapshot, CloudSaveTransport, RemoteSaveSnapshot}; diff --git a/desktop/src-tauri/cloud_saves/src/ludusavi.rs b/desktop/src-tauri/cloud_saves/src/ludusavi.rs index 5ead264df..7f864be96 100644 --- a/desktop/src-tauri/cloud_saves/src/ludusavi.rs +++ b/desktop/src-tauri/cloud_saves/src/ludusavi.rs @@ -334,7 +334,71 @@ pub fn unpack_save_archive( Ok(()) } -/// Pre-launch hook: checks for newer remote cloud save snapshot and restores it. +/// Path to the locally cached archive for a game. +pub fn cache_archive_path(game_id: &str) -> PathBuf { + DATA_ROOT_DIR + .join("saves") + .join("cache") + .join(format!("{game_id}.tar.zst")) +} + +/// SHA-256 of a file's contents, used to verify archives in transit. +pub fn sha256_file(path: &Path) -> Result { + let mut file = File::open(path)?; + let mut hasher = Sha256::new(); + let mut buffer = [0u8; 8192]; + loop { + let read = file.read(&mut buffer)?; + if read == 0 { + break; + } + hasher.update(&buffer[..read]); + } + Ok(format!("{:x}", hasher.finalize())) +} + +/// Runs Ludusavi discovery and packs the found saves into the cache archive. +/// Returns `(bytes, checksum)`, or `None` when Ludusavi is unavailable or the +/// game has no detectable saves. +pub fn backup_saves_to_cache( + context: &CloudSaveSyncContext, +) -> Result, BackupError> { + let client = match LudusaviClient::discover() { + Some(c) => c, + None => { + debug!("Ludusavi binary not found; skipping cloud save backup"); + return Ok(None); + } + }; + + let staging_dir = DATA_ROOT_DIR + .join("saves") + .join("staging") + .join(&context.game_id); + if staging_dir.exists() { + let _ = fs::remove_dir_all(&staging_dir); + } + fs::create_dir_all(&staging_dir)?; + + let backup_resp = client.backup_game( + &context.game_title, + &staging_dir, + context.wine_prefix.as_deref(), + )?; + if backup_resp.overall.total_games == 0 && backup_resp.overall.total_bytes == 0 { + debug!( + "No save files found by Ludusavi for game {}", + context.game_title + ); + return Ok(None); + } + + let archive_path = cache_archive_path(&context.game_id); + let (bytes, checksum) = pack_save_archive(&staging_dir, &archive_path)?; + Ok(Some((bytes, checksum))) +} + +/// Pre-launch hook: restores the locally cached cloud save snapshot, if present. pub fn sync_pre_launch(context: &CloudSaveSyncContext) -> Result { let game_id = context.game_id.as_str(); let game_title = context.game_title.as_str(); @@ -353,10 +417,7 @@ pub fn sync_pre_launch(context: &CloudSaveSyncContext) -> Result Result Result { - let game_id = context.game_id.as_str(); - let game_title = context.game_title.as_str(); info!( "Cloud save post-exit backup for game '{}' ({})", - game_title, game_id + context.game_title, context.game_id ); - let client = match LudusaviClient::discover() { - Some(c) => c, - None => { - debug!("Ludusavi binary not found; skipping cloud save post-exit hook"); - return Ok(false); + match backup_saves_to_cache(context)? { + Some((bytes, checksum)) => { + info!( + "Packaged cloud save archive for {} ({} bytes, sha256: {})", + context.game_title, bytes, checksum + ); + Ok(true) } - }; - - let staging_dir = DATA_ROOT_DIR.join("saves").join("staging").join(game_id); - if staging_dir.exists() { - let _ = fs::remove_dir_all(&staging_dir); - } - fs::create_dir_all(&staging_dir)?; - - let backup_resp = - client.backup_game(game_title, &staging_dir, context.wine_prefix.as_deref())?; - if backup_resp.overall.total_games == 0 && backup_resp.overall.total_bytes == 0 { - debug!("No save files found by Ludusavi for game {}", game_title); - return Ok(false); + None => Ok(false), } - - let cache_dir = DATA_ROOT_DIR.join("saves").join("cache"); - fs::create_dir_all(&cache_dir)?; - let archive_path = cache_dir.join(format!("{game_id}.tar.zst")); - - let (bytes, checksum) = pack_save_archive(&staging_dir, &archive_path)?; - info!( - "Packaged cloud save archive for {} ({} bytes, sha256: {})", - game_title, bytes, checksum - ); - - Ok(true) } #[cfg(test)] diff --git a/desktop/src-tauri/cloud_saves/src/sync.rs b/desktop/src-tauri/cloud_saves/src/sync.rs new file mode 100644 index 000000000..99cdb74fc --- /dev/null +++ b/desktop/src-tauri/cloud_saves/src/sync.rs @@ -0,0 +1,248 @@ +use std::fs; +use std::path::PathBuf; + +use log::{info, warn}; + +use database::db::DATA_ROOT_DIR; + +use crate::error::BackupError; +use crate::ludusavi::{ + backup_saves_to_cache, cache_archive_path, sha256_file, unpack_save_archive, + CloudSaveSyncContext, LudusaviClient, +}; +use crate::transport::CloudSaveTransport; + +/// Slot index used by the automatic Steam-Cloud-style sync. +pub const DEFAULT_SLOT: u32 = 1; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SyncAction { + NoOp, + Pulled, + Pushed, + ConflictArchived, +} + +/// Outcome of comparing the remote snapshot against local state before launch. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum PreLaunchDecision { + /// Local cache already matches the remote snapshot. + Skip, + /// Remote is newer and local has no unsynced changes: fast-forward. + Pull, + /// Local was played offline and diverged from the remote snapshot. + Conflict, +} + +/// Pure conflict decision: safe forking (keep local) when the local cache has +/// unsynced changes, otherwise fast-forward to the remote snapshot. +pub fn decide_pre_launch( + remote_checksum: &str, + cache_checksum: Option<&str>, + last_uploaded_checksum: Option<&str>, +) -> PreLaunchDecision { + if cache_checksum == Some(remote_checksum) { + return PreLaunchDecision::Skip; + } + + let local_dirty = match (cache_checksum, last_uploaded_checksum) { + (Some(current), Some(last)) => current != last, + (Some(_), None) => true, + _ => false, + }; + + if local_dirty { + PreLaunchDecision::Conflict + } else { + PreLaunchDecision::Pull + } +} + +#[derive(Default, serde::Serialize, serde::Deserialize)] +struct LocalSyncState { + last_uploaded_checksum: Option, +} + +fn state_path(game_id: &str) -> PathBuf { + DATA_ROOT_DIR + .join("saves") + .join("state") + .join(format!("{game_id}.json")) +} + +fn read_state(game_id: &str) -> LocalSyncState { + fs::read_to_string(state_path(game_id)) + .ok() + .and_then(|raw| serde_json::from_str(&raw).ok()) + .unwrap_or_default() +} + +fn write_state(game_id: &str, state: &LocalSyncState) -> Result<(), BackupError> { + let path = state_path(game_id); + if let Some(parent) = path.parent() { + fs::create_dir_all(parent)?; + } + fs::write(path, serde_json::to_string(state)?)?; + Ok(()) +} + +fn rollback_path(game_id: &str) -> PathBuf { + let timestamp = chrono::Utc::now().format("%Y%m%dT%H%M%S"); + DATA_ROOT_DIR + .join("saves") + .join("rollback") + .join(format!("{game_id}-{timestamp}.tar.zst")) +} + +/// Pre-launch sync against the server, applying the hybrid conflict policy: +/// auto-archive the remote snapshot into a timestamped rollback when the local +/// cache has unsynced changes (the UI surfaces the conflict), otherwise pull +/// and restore the remote snapshot. +pub fn sync_pre_launch_with( + transport: &dyn CloudSaveTransport, + context: &CloudSaveSyncContext, + slot: u32, +) -> Result { + let Some(remote) = transport.fetch_latest_snapshot(&context.game_id, slot)? else { + return Ok(SyncAction::NoOp); + }; + + let cache = cache_archive_path(&context.game_id); + let cache_checksum = if cache.is_file() { + Some(sha256_file(&cache)?) + } else { + None + }; + let state = read_state(&context.game_id); + + match decide_pre_launch( + &remote.checksum, + cache_checksum.as_deref(), + state.last_uploaded_checksum.as_deref(), + ) { + PreLaunchDecision::Skip => Ok(SyncAction::NoOp), + PreLaunchDecision::Conflict => { + let rollback = rollback_path(&context.game_id); + transport.download_snapshot(&remote, &rollback)?; + warn!( + "Cloud save conflict for {}: archived remote snapshot to {:?}", + context.game_id, rollback + ); + Ok(SyncAction::ConflictArchived) + } + PreLaunchDecision::Pull => { + let staged = cache.with_extension("remote.tar.zst"); + transport.download_snapshot(&remote, &staged)?; + + let actual = sha256_file(&staged)?; + if !actual.eq_ignore_ascii_case(&remote.checksum) { + return Err(BackupError::ChecksumMismatch { + expected: remote.checksum.clone(), + actual, + }); + } + + if let Some(parent) = cache.parent() { + fs::create_dir_all(parent)?; + } + fs::copy(&staged, &cache)?; + let _ = fs::remove_file(&staged); + + if let Some(client) = LudusaviClient::discover() { + let staging = DATA_ROOT_DIR + .join("saves") + .join("staging") + .join(&context.game_id); + if staging.exists() { + let _ = fs::remove_dir_all(&staging); + } + fs::create_dir_all(&staging)?; + unpack_save_archive(&cache, &staging, Some(&remote.checksum))?; + client.restore_game( + &context.game_title, + &staging, + context.wine_prefix.as_deref(), + )?; + } + + write_state( + &context.game_id, + &LocalSyncState { + last_uploaded_checksum: Some(remote.checksum.clone()), + }, + )?; + info!("Pulled cloud save snapshot for {}", context.game_id); + Ok(SyncAction::Pulled) + } + } +} + +/// Post-exit sync: run Ludusavi discovery, pack the archive, and upload it. +pub fn sync_post_exit_with( + transport: &dyn CloudSaveTransport, + context: &CloudSaveSyncContext, + slot: u32, +) -> Result { + let Some((_bytes, checksum)) = backup_saves_to_cache(context)? else { + return Ok(SyncAction::NoOp); + }; + + let archive = cache_archive_path(&context.game_id); + transport.upload_snapshot(&context.game_id, slot, &archive, &checksum)?; + write_state( + &context.game_id, + &LocalSyncState { + last_uploaded_checksum: Some(checksum.clone()), + }, + )?; + info!( + "Uploaded cloud save snapshot for {} (sha256: {})", + context.game_id, checksum + ); + Ok(SyncAction::Pushed) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn skip_when_cache_matches_remote() { + assert_eq!( + decide_pre_launch("abc", Some("abc"), Some("abc")), + PreLaunchDecision::Skip + ); + } + + #[test] + fn pull_when_remote_newer_and_local_clean() { + assert_eq!( + decide_pre_launch("remote", Some("cache"), Some("cache")), + PreLaunchDecision::Pull + ); + } + + #[test] + fn conflict_when_local_has_unsynced_changes() { + assert_eq!( + decide_pre_launch("remote", Some("local-dirty"), Some("last-upload")), + PreLaunchDecision::Conflict + ); + } + + #[test] + fn conflict_when_local_cache_exists_without_upload_history() { + assert_eq!( + decide_pre_launch("remote", Some("local"), None), + PreLaunchDecision::Conflict + ); + } + + #[test] + fn pull_when_there_is_no_local_cache() { + assert_eq!( + decide_pre_launch("remote", None, None), + PreLaunchDecision::Pull + ); + } +} diff --git a/desktop/src-tauri/cloud_saves/src/transport.rs b/desktop/src-tauri/cloud_saves/src/transport.rs new file mode 100644 index 000000000..e52531b8f --- /dev/null +++ b/desktop/src-tauri/cloud_saves/src/transport.rs @@ -0,0 +1,93 @@ +use std::path::Path; + +use crate::error::BackupError; + +/// A single remote cloud-save snapshot: the storage object id and the SHA-256 +/// of its plaintext archive. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RemoteSaveSnapshot { + pub object_id: String, + pub checksum: String, +} + +/// Transport used by the sync engine to reach the Drop server. Kept as a trait +/// so the orchestration can be unit-tested without a network and so the desktop +/// layer owns the authenticated HTTP implementation. +pub trait CloudSaveTransport: Send + Sync { + /// Latest snapshot for `(game_id, slot)`, or `None` when no save exists. + fn fetch_latest_snapshot( + &self, + game_id: &str, + slot: u32, + ) -> Result, BackupError>; + + /// Download a snapshot's archive bytes into `dest`. + fn download_snapshot( + &self, + snapshot: &RemoteSaveSnapshot, + dest: &Path, + ) -> Result<(), BackupError>; + + /// Upload `archive` for `(game_id, slot)`, expected to hash to `checksum`. + fn upload_snapshot( + &self, + game_id: &str, + slot: u32, + archive: &Path, + checksum: &str, + ) -> Result<(), BackupError>; +} + +#[derive(serde::Deserialize)] +#[serde(rename_all = "camelCase")] +struct SaveSlotResponse { + #[serde(default)] + history_object_ids: Vec, + #[serde(default)] + history_checksums: Vec, +} + +/// Parses the JSON body of `GET /api/v1/client/saves/:gameid/:slotindex` and +/// returns its newest snapshot, if any. +pub fn parse_latest_snapshot(json: &str) -> Result, BackupError> { + let slot: SaveSlotResponse = serde_json::from_str(json)?; + match ( + slot.history_object_ids.last(), + slot.history_checksums.last(), + ) { + (Some(object_id), Some(checksum)) => Ok(Some(RemoteSaveSnapshot { + object_id: object_id.clone(), + checksum: checksum.clone(), + })), + _ => Ok(None), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parses_the_newest_snapshot() { + let json = r#"{ + "index": 1, + "historyObjectIds": ["obj-old", "obj-new"], + "historyChecksums": ["hash-old", "hash-new"] + }"#; + let snapshot = parse_latest_snapshot(json).unwrap().unwrap(); + assert_eq!(snapshot.object_id, "obj-new"); + assert_eq!(snapshot.checksum, "hash-new"); + } + + #[test] + fn empty_history_yields_no_snapshot() { + let json = r#"{ "index": 1, "historyObjectIds": [], "historyChecksums": [] }"#; + assert!(parse_latest_snapshot(json).unwrap().is_none()); + } + + #[test] + fn malformed_json_is_a_serialization_error() { + let err = parse_latest_snapshot("not json").unwrap_err(); + assert!(matches!(err, BackupError::SerializationError(_))); + } +} diff --git a/desktop/src-tauri/process/Cargo.toml b/desktop/src-tauri/process/Cargo.toml index 8b5a20dd4..4c7892c45 100644 --- a/desktop/src-tauri/process/Cargo.toml +++ b/desktop/src-tauri/process/Cargo.toml @@ -14,6 +14,7 @@ keyvalues-parser = "0.2.3" log = "0.4.28" page_size = "0.6.0" regex = "1.11" +remote = { path = "../remote", version = "0.1.0" } serde = "1.0.228" serde_json = "1" serde_with = "3.15.0" diff --git a/desktop/src-tauri/process/src/cloud_save_transport.rs b/desktop/src-tauri/process/src/cloud_save_transport.rs new file mode 100644 index 000000000..51d47871d --- /dev/null +++ b/desktop/src-tauri/process/src/cloud_save_transport.rs @@ -0,0 +1,103 @@ +use std::path::Path; + +use cloud_saves::error::BackupError; +use cloud_saves::transport::{parse_latest_snapshot, CloudSaveTransport, RemoteSaveSnapshot}; +use remote::auth::generate_authorization_header; +use remote::requests::generate_url; +use remote::utils::DROP_CLIENT_SYNC; + +fn to_error(error: E) -> BackupError { + BackupError::ExecutionError(error.to_string()) +} + +/// Authenticated Drop-server transport for the cloud save sync engine. +pub struct DropServerTransport; + +impl CloudSaveTransport for DropServerTransport { + fn fetch_latest_snapshot( + &self, + game_id: &str, + slot: u32, + ) -> Result, BackupError> { + let url = generate_url(&["/api/v1/client/saves", game_id, &slot.to_string()], &[]) + .map_err(to_error)?; + + let response = DROP_CLIENT_SYNC + .get(url) + .header("Authorization", generate_authorization_header()) + .send() + .map_err(to_error)?; + + if response.status().as_u16() == 404 { + return Ok(None); + } + if !response.status().is_success() { + return Err(BackupError::ExecutionError(format!( + "cloud save metadata request failed: {}", + response.status() + ))); + } + + let body = response.text().map_err(to_error)?; + parse_latest_snapshot(&body) + } + + fn download_snapshot( + &self, + snapshot: &RemoteSaveSnapshot, + dest: &Path, + ) -> Result<(), BackupError> { + let url = + generate_url(&["/api/v1/client/object", &snapshot.object_id], &[]).map_err(to_error)?; + + let response = DROP_CLIENT_SYNC + .get(url) + .header("Authorization", generate_authorization_header()) + .send() + .map_err(to_error)?; + + if !response.status().is_success() { + return Err(BackupError::ExecutionError(format!( + "cloud save download failed: {}", + response.status() + ))); + } + + let bytes = response.bytes().map_err(to_error)?; + if let Some(parent) = dest.parent() { + std::fs::create_dir_all(parent)?; + } + std::fs::write(dest, bytes)?; + Ok(()) + } + + fn upload_snapshot( + &self, + game_id: &str, + slot: u32, + archive: &Path, + _checksum: &str, + ) -> Result<(), BackupError> { + let url = generate_url( + &["/api/v1/client/saves", game_id, &slot.to_string(), "push"], + &[], + ) + .map_err(to_error)?; + + let file = std::fs::File::open(archive)?; + let response = DROP_CLIENT_SYNC + .post(url) + .header("Authorization", generate_authorization_header()) + .body(file) + .send() + .map_err(to_error)?; + + if !response.status().is_success() { + return Err(BackupError::ExecutionError(format!( + "cloud save upload failed: {}", + response.status() + ))); + } + Ok(()) + } +} diff --git a/desktop/src-tauri/process/src/lib.rs b/desktop/src-tauri/process/src/lib.rs index 1a5dfe277..fb4d8c0b0 100644 --- a/desktop/src-tauri/process/src/lib.rs +++ b/desktop/src-tauri/process/src/lib.rs @@ -16,6 +16,7 @@ pub static PROCESS_MANAGER: ProcessManagerWrapper = ProcessManagerWrapper::new() #[cfg(target_os = "linux")] pub mod compat; +pub mod cloud_save_transport; pub mod error; pub mod format; pub mod interceptor; diff --git a/desktop/src-tauri/process/src/process_manager.rs b/desktop/src-tauri/process/src/process_manager.rs index f2d234516..19494f67b 100644 --- a/desktop/src-tauri/process/src/process_manager.rs +++ b/desktop/src-tauri/process/src/process_manager.rs @@ -222,11 +222,29 @@ impl ProcessManager<'_> { // Cloud saves post-exit hook: detect saves and stage/archive a backup. // Build the context and drop the read guard before doing filesystem or // external-tool work so the database lock is not held across it. - let cloud_context = { + let (cloud_context, authenticated) = { let db_handle = borrow_db_checked(); - Self::cloud_save_context(&db_handle, &game_id, &process.install_dir) + ( + Self::cloud_save_context(&db_handle, &game_id, &process.install_dir), + db_handle.auth.is_some(), + ) + }; + let post_exit_result = if authenticated { + cloud_saves::sync_post_exit_with( + &crate::cloud_save_transport::DropServerTransport, + &cloud_context, + cloud_saves::DEFAULT_SLOT, + ) + } else { + cloud_saves::sync_post_exit(&cloud_context).map(|did| { + if did { + cloud_saves::SyncAction::Pushed + } else { + cloud_saves::SyncAction::NoOp + } + }) }; - if let Err(e) = cloud_saves::CloudSaveSyncManager::new(cloud_context).sync_post_exit() { + if let Err(e) = post_exit_result { warn!("Cloud save post-exit backup failed for {game_id}: {e:?}"); } @@ -731,10 +749,26 @@ impl ProcessManager<'_> { interceptor.pre_launch(&meta.id, &launch_parameters.1, &mut command)?; } - // Cloud saves pre-launch hook: check and restore cloud saves if newer + // Cloud saves pre-launch hook: pull and restore the newest snapshot when + // remote sync is available, otherwise restore the local cache. let cloud_context = Self::cloud_save_context(&db_lock, &meta.id, &launch_parameters.1); - if let Err(e) = cloud_saves::CloudSaveSyncManager::new(cloud_context).sync_pre_launch() { + let pre_launch_result = if db_lock.auth.is_some() { + cloud_saves::sync_pre_launch_with( + &crate::cloud_save_transport::DropServerTransport, + &cloud_context, + cloud_saves::DEFAULT_SLOT, + ) + } else { + cloud_saves::sync_pre_launch(&cloud_context).map(|did| { + if did { + cloud_saves::SyncAction::Pulled + } else { + cloud_saves::SyncAction::NoOp + } + }) + }; + if let Err(e) = pre_launch_result { warn!("Cloud save pre-launch sync warning for {}: {e:?}", meta.id); } diff --git a/desktop/src-tauri/src/cloud_saves.rs b/desktop/src-tauri/src/cloud_saves.rs new file mode 100644 index 000000000..ef3964160 --- /dev/null +++ b/desktop/src-tauri/src/cloud_saves.rs @@ -0,0 +1,101 @@ +use database::{borrow_db_checked, db::DATA_ROOT_DIR}; +use remote::auth::generate_authorization_header; +use remote::requests::generate_url; +use remote::utils::DROP_CLIENT_ASYNC; + +#[derive(serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CloudSaveSlotView { + pub index: i32, + pub history_count: usize, + pub latest_object_id: Option, + pub latest_checksum: Option, + pub last_used_client_id: Option, + pub created_at: Option, +} + +#[derive(serde::Deserialize)] +#[serde(rename_all = "camelCase")] +struct SaveSlotRow { + index: i32, + #[serde(default)] + history_object_ids: Vec, + #[serde(default)] + history_checksums: Vec, + #[serde(default)] + last_used_client_id: Option, + #[serde(default)] + created_at: Option, +} + +fn ensure_authenticated() -> Result<(), String> { + if borrow_db_checked().auth.is_none() { + return Err("not authenticated".to_string()); + } + Ok(()) +} + +/// Lists the caller's cloud save slots for a game. +#[tauri::command] +pub async fn fetch_cloud_save_slots(game_id: String) -> Result, String> { + ensure_authenticated()?; + + let url = generate_url(&["/api/v1/client/saves", &game_id], &[]).map_err(|e| e.to_string())?; + let response = DROP_CLIENT_ASYNC + .get(url) + .header("Authorization", generate_authorization_header()) + .send() + .await + .map_err(|e| e.to_string())?; + + if !response.status().is_success() { + return Err(format!( + "failed to fetch cloud save slots: {}", + response.status() + )); + } + + let body = response.text().await.map_err(|e| e.to_string())?; + let rows: Vec = serde_json::from_str(&body).map_err(|e| e.to_string())?; + + Ok(rows + .into_iter() + .map(|row| CloudSaveSlotView { + index: row.index, + history_count: row.history_object_ids.len(), + latest_object_id: row.history_object_ids.last().cloned(), + latest_checksum: row.history_checksums.last().cloned(), + last_used_client_id: row.last_used_client_id, + created_at: row.created_at, + }) + .collect()) +} + +/// Downloads a snapshot object into `/saves/downloads` and returns its path. +#[tauri::command] +pub async fn download_cloud_save_object(object_id: String) -> Result { + ensure_authenticated()?; + + let url = + generate_url(&["/api/v1/client/object", &object_id], &[]).map_err(|e| e.to_string())?; + let response = DROP_CLIENT_ASYNC + .get(url) + .header("Authorization", generate_authorization_header()) + .send() + .await + .map_err(|e| e.to_string())?; + + if !response.status().is_success() { + return Err(format!( + "failed to download cloud save object: {}", + response.status() + )); + } + + let bytes = response.bytes().await.map_err(|e| e.to_string())?; + let dir = DATA_ROOT_DIR.join("saves").join("downloads"); + std::fs::create_dir_all(&dir).map_err(|e| e.to_string())?; + let path = dir.join(format!("{object_id}.tar.zst")); + std::fs::write(&path, bytes).map_err(|e| e.to_string())?; + Ok(path.to_string_lossy().to_string()) +} diff --git a/desktop/src-tauri/src/lib.rs b/desktop/src-tauri/src/lib.rs index 9f8b409ac..673414c76 100644 --- a/desktop/src-tauri/src/lib.rs +++ b/desktop/src-tauri/src/lib.rs @@ -59,6 +59,7 @@ use url::Url; use utils::app_emit; mod client; +mod cloud_saves; mod collections; mod download_manager; mod downloads; @@ -71,6 +72,7 @@ mod settings; mod updates; use client::*; +use cloud_saves::*; use download_manager::*; use downloads::*; use games::*; @@ -281,6 +283,9 @@ pub fn run() { resume_downloads, cancel_game, uninstall_game, + // Cloud saves + fetch_cloud_save_slots, + download_cloud_save_object, // Processes launch_game, kill_game, diff --git a/server/server/internal/saves/__tests__/saves.test.ts b/server/server/internal/saves/__tests__/saves.test.ts index 2515d7b3e..cdc760076 100644 --- a/server/server/internal/saves/__tests__/saves.test.ts +++ b/server/server/internal/saves/__tests__/saves.test.ts @@ -20,6 +20,7 @@ const slots = new Map(); const objects = new Map(); const deleted: string[] = []; let historyLimit = 3; +let sizeLimitMb = 10; const prisma: SaveManagerDeps["prisma"] = { saveSlot: { @@ -82,7 +83,8 @@ const objectHandler: SaveManagerDeps["objectHandler"] = { }; const settings: SaveManagerDeps["settings"] = { - get: async () => historyLimit, + get: async (key) => + key === "saveSlotSizeLimit" ? sizeLimitMb : historyLimit, }; const saveManager = new SaveManager({ prisma, objectHandler, settings }); @@ -165,3 +167,34 @@ test("SaveManager.pushSave rejects an unknown save slot", async () => { /Save not found/, ); }); + +test("SaveManager.pushSave rejects payloads over saveSlotSizeLimit and cleans up", async () => { + slots.clear(); + objects.clear(); + deleted.length = 0; + historyLimit = 3; + sizeLimitMb = 0.000001; + + const userId = "user-1"; + const gameId = "game-too-big"; + const index = 1; + slots.set(keyOf({ userId, gameId, index }), { + historyObjectIds: [], + historyChecksums: [], + }); + + await assert.rejects( + () => + saveManager.pushSave( + gameId, + userId, + index, + Readable.from(Buffer.from("too big to fit")), + ), + /saveSlotSizeLimit/, + ); + + assert.equal(deleted.length, 1); + const record = slots.get(keyOf({ userId, gameId, index })); + assert.equal(record?.historyObjectIds.length, 0); +}); diff --git a/server/server/internal/saves/manager.ts b/server/server/internal/saves/manager.ts index 4c571616c..0236b8e93 100644 --- a/server/server/internal/saves/manager.ts +++ b/server/server/internal/saves/manager.ts @@ -42,7 +42,7 @@ export interface SaveManagerDeps { deleteAsSystem(id: string): Promise; }; settings: { - get(key: "saveSlotHistoryLimit"): Promise; + get(key: "saveSlotHistoryLimit" | "saveSlotSizeLimit"): Promise; }; } @@ -87,7 +87,9 @@ export class SaveManager { const newSaveStream = await this.deps.objectHandler.createWithStream( newSaveObjectId, { saveSlot: JSON.stringify({ userId, gameId, index }) }, - [], + // System-tracked object: grant the owner read access (same convention as + // screenshots) so the desktop client can pull the archive back down. + [`${userId}:read`], ); if (!newSaveStream) throw createError({ @@ -96,10 +98,23 @@ export class SaveManager { }); const hashStream = createHash("sha256"); - stream.on("data", (chunk: Buffer) => hashStream.update(chunk)); + let totalBytes = 0; + stream.on("data", (chunk: Buffer) => { + hashStream.update(chunk); + totalBytes += chunk.length; + }); await Stream.promises.pipeline(stream, newSaveStream); + const sizeLimitMb = await this.deps.settings.get("saveSlotSizeLimit"); + if (totalBytes > sizeLimitMb * 1024 * 1024) { + await this.deps.objectHandler.deleteAsSystem(newSaveObjectId); + throw createError({ + statusCode: 413, + statusMessage: "Save exceeds saveSlotSizeLimit", + }); + } + const hash = hashStream.digest("hex"); if (!hash) { From dad16edb04ac93824db9efb2447dd5a467be6346 Mon Sep 17 00:00:00 2001 From: John Smith Date: Mon, 14 Sep 2026 01:28:37 -0400 Subject: [PATCH 169/243] feat(delivery): add downpour push CDN chunker and LAN peer discovery contract (#17) - Add `downpour push`: content-defined (buzhash) chunking with zstd compression, content-addressed chunk output, and a JSON manifest. Unit-tested for reassembly, min/max bounds, determinism, and manifest round-trip. - Add a torrential LAN P2P contract: LanPeer registry with TTL eviction and latency-dominant peer scoring, exposing the discovery/trait boundary that the mDNS/SSDP backend and QUIC transport implement. --- cli/Cargo.lock | 32 ++++++ cli/Cargo.toml | 6 ++ cli/src/cli.rs | 9 ++ cli/src/commands/mod.rs | 1 + cli/src/commands/push/chunker.rs | 120 +++++++++++++++++++++ cli/src/commands/push/mod.rs | 173 +++++++++++++++++++++++++++++++ cli/src/main.rs | 10 ++ torrential/src/lan/mod.rs | 136 ++++++++++++++++++++++++ torrential/src/lib.rs | 1 + 9 files changed, 488 insertions(+) create mode 100644 cli/src/commands/push/chunker.rs create mode 100644 cli/src/commands/push/mod.rs create mode 100644 torrential/src/lan/mod.rs diff --git a/cli/Cargo.lock b/cli/Cargo.lock index 9e7941564..ef25aea81 100644 --- a/cli/Cargo.lock +++ b/cli/Cargo.lock @@ -675,8 +675,12 @@ dependencies = [ "rand 0.9.5", "serde", "serde_json", + "sha2 0.10.9", + "tempfile", "tokio", "tokio-util", + "walkdir", + "zstd", ] [[package]] @@ -3266,3 +3270,31 @@ name = "zmij" version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" + +[[package]] +name = "zstd" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64d80649ab6db9d9f6f9c80a40becd948eda4714a0a5ac8c4d157a32231c7882" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.1.0+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ef0a8027ec3ee71300ab3bcbcd0393f434aa72b91ca6d635a39941deae8eea0" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 1dc96d7c6..b15a26277 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -19,5 +19,11 @@ opendal = { version = "0.58.2", features = ["services-s3"] } rand = "0.9.3" serde = { version = "1.0.228", features = ["derive"] } serde_json = "1.0.148" +sha2 = "0.10" +walkdir = "2.5" +zstd = "0.13" tokio = { version = "1.48.0", features = ["fs", "macros"] } tokio-util = { version = "0.7.18", features = ["compat"] } + +[dev-dependencies] +tempfile = "3" diff --git a/cli/src/cli.rs b/cli/src/cli.rs index 318fc457b..8069cddfe 100644 --- a/cli/src/cli.rs +++ b/cli/src/cli.rs @@ -30,6 +30,15 @@ pub enum Commands { /// Alias of a given connection name: Option, }, + /// Publishes a local depot directory as content-addressed chunks + Push { + /// Directory to publish + #[arg(short, long, default_value_t = String::from("."))] + path: String, + /// Output directory for chunks + manifest + #[arg(short, long, default_value_t = String::from("dist"))] + out: String, + }, } #[derive(Args)] diff --git a/cli/src/commands/mod.rs b/cli/src/commands/mod.rs index 0a9a925a4..3160ddeb0 100644 --- a/cli/src/commands/mod.rs +++ b/cli/src/commands/mod.rs @@ -1,2 +1,3 @@ pub mod connect; +pub mod push; pub mod upload; diff --git a/cli/src/commands/push/chunker.rs b/cli/src/commands/push/chunker.rs new file mode 100644 index 000000000..4eefdf29e --- /dev/null +++ b/cli/src/commands/push/chunker.rs @@ -0,0 +1,120 @@ +//! Content-defined chunking for the `downpour push` publisher. +//! +//! Uses a rolling buzhash so chunk boundaries follow file content rather than +//! fixed offsets: inserting bytes near the start of a file only re-chunks the +//! region around the edit, which keeps depot deltas small. + +/// Smallest chunk emitted (except the final chunk of a file). +pub const DEFAULT_MIN_CHUNK: usize = 64 * 1024; +/// Largest chunk emitted; forces a boundary to bound memory and blob size. +pub const DEFAULT_MAX_CHUNK: usize = 1024 * 1024; +/// Boundary mask size: average chunk target is `2^DEFAULT_AVG_BITS`. +pub const DEFAULT_AVG_BITS: u32 = 20; + +/// Rolling content-defined chunker. +pub struct Chunker { + min: usize, + max: usize, + mask: u64, + table: [u64; 256], +} + +impl Default for Chunker { + fn default() -> Self { + Self::new(DEFAULT_MIN_CHUNK, DEFAULT_MAX_CHUNK, DEFAULT_AVG_BITS) + } +} + +impl Chunker { + pub fn new(min: usize, max: usize, avg_bits: u32) -> Self { + assert!(min > 0 && min <= max, "invalid chunk bounds"); + let mut state: u64 = 0x9E37_79B9_7F4A_7C15; + let mut table = [0u64; 256]; + for entry in table.iter_mut() { + // Deterministic xorshift64 so boundaries are reproducible everywhere. + state ^= state << 13; + state ^= state >> 7; + state ^= state << 17; + *entry = state; + } + Self { + min, + max, + mask: (1u64 << avg_bits) - 1, + table, + } + } + + /// Splits `data` into content-defined chunks, returned as `(start, end)`. + pub fn split<'a>(&self, data: &'a [u8]) -> Vec<&'a [u8]> { + let mut chunks = Vec::new(); + let mut start = 0usize; + while start < data.len() { + let end = self.next_boundary(&data[start..]); + chunks.push(&data[start..start + end]); + start += end; + } + if chunks.is_empty() { + chunks.push(&data[0..0]); + } + chunks + } + + fn next_boundary(&self, data: &[u8]) -> usize { + if data.len() <= self.min { + return data.len(); + } + + let limit = data.len().min(self.max); + let mut hash: u64 = 0; + for (index, byte) in data[..limit].iter().enumerate() { + hash = hash.rotate_left(1) ^ self.table[*byte as usize]; + if index + 1 >= self.min && (hash & self.mask) == 0 { + return index + 1; + } + } + limit + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn chunks_reassemble_to_the_input() { + let data: Vec = (0..200_000u32).map(|i| (i % 251) as u8).collect(); + let chunker = Chunker::new(1024, 8192, 10); + let chunks = chunker.split(&data); + let rejoined: Vec = chunks.iter().flat_map(|c| c.iter().copied()).collect(); + assert_eq!(rejoined, data); + assert!(chunks.len() > 1, "expected multiple chunks"); + } + + #[test] + fn respects_min_and_max_bounds() { + let data: Vec = (0..100_000u32).map(|i| (i * 7 % 256) as u8).collect(); + let chunker = Chunker::new(2048, 4096, 9); + let chunks = chunker.split(&data); + for chunk in &chunks[..chunks.len().saturating_sub(1)] { + assert!(chunk.len() >= 2048, "chunk below minimum: {}", chunk.len()); + assert!(chunk.len() <= 4096, "chunk above maximum: {}", chunk.len()); + } + } + + #[test] + fn is_deterministic_for_the_same_input() { + let data: Vec = (0..50_000u32).map(|i| (i % 97) as u8).collect(); + let a = Chunker::default().split(&data); + let b = Chunker::default().split(&data); + assert_eq!(a.len(), b.len()); + assert!(a.iter().zip(&b).all(|(x, y)| x == y)); + } + + #[test] + fn empty_input_still_yields_one_empty_chunk() { + let chunks = Chunker::default().split(&[]); + assert_eq!(chunks.len(), 1); + assert!(chunks[0].is_empty()); + } +} diff --git a/cli/src/commands/push/mod.rs b/cli/src/commands/push/mod.rs new file mode 100644 index 000000000..ed607ec4f --- /dev/null +++ b/cli/src/commands/push/mod.rs @@ -0,0 +1,173 @@ +//! `downpour push`: publishes a local depot directory as content-addressed, +//! zstd-compressed chunks plus a JSON manifest. +//! +//! This is the local/offline half of Phase 1 content delivery (#17): it produces +//! the same chunk layout the depot server ingests, so a later S3/HTTP uploader +//! only has to stream the emitted objects. + +pub mod chunker; + +use std::fs; +use std::path::{Path, PathBuf}; + +use anyhow::{Context, Result}; +use serde::Serialize; +use sha2::{Digest, Sha256}; +use walkdir::WalkDir; + +use self::chunker::Chunker; + +fn hex_digest(bytes: &[u8]) -> String { + use std::fmt::Write as _; + let mut out = String::with_capacity(64); + for byte in Sha256::digest(bytes) { + let _ = write!(out, "{byte:02x}"); + } + out +} + +#[derive(Debug, Serialize)] +pub struct PushSummary { + pub files: usize, + pub chunks: usize, + pub bytes_in: u64, + pub bytes_out: u64, +} + +#[derive(Debug, Serialize)] +struct ChunkRef { + sha256: String, + original_len: usize, + compressed_len: usize, +} + +#[derive(Debug, Serialize)] +struct ManifestEntry { + path: String, + chunks: Vec, +} + +#[derive(Debug, Serialize)] +struct PushManifest { + version: u32, + chunker: &'static str, + entries: Vec, +} + +/// Chunks `path` into `out/chunks` and writes `out/manifest.json`. +pub fn run(path: &Path, out: &Path) -> Result { + let chunker = Chunker::default(); + let chunk_dir = out.join("chunks"); + fs::create_dir_all(&chunk_dir).context("creating chunk output directory")?; + + let mut entries = Vec::new(); + let mut summary = PushSummary { + files: 0, + chunks: 0, + bytes_in: 0, + bytes_out: 0, + }; + let mut written: std::collections::HashSet = std::collections::HashSet::new(); + + for entry in WalkDir::new(path).into_iter().filter_map(|e| e.ok()) { + if !entry.file_type().is_file() { + continue; + } + let relative = entry + .path() + .strip_prefix(path) + .context("stripping root prefix")? + .to_string_lossy() + .replace('\\', "/"); + let data = fs::read(entry.path()) + .with_context(|| format!("reading {}", entry.path().display()))?; + + summary.files += 1; + summary.bytes_in += data.len() as u64; + + let mut chunks = Vec::new(); + for chunk in chunker.split(&data) { + let compressed = zstd::bulk::compress(chunk, 3).context("compressing chunk")?; + let digest = hex_digest(chunk); + let chunk_path = chunk_dir.join(&digest); + if !written.contains(&digest) { + fs::write(&chunk_path, &compressed).context("writing chunk")?; + written.insert(digest.clone()); + summary.bytes_out += compressed.len() as u64; + } + summary.chunks += 1; + chunks.push(ChunkRef { + sha256: digest, + original_len: chunk.len(), + compressed_len: compressed.len(), + }); + } + + entries.push(ManifestEntry { + path: relative, + chunks, + }); + } + + let manifest = PushManifest { + version: 1, + chunker: "buzhash-cdc-v1", + entries, + }; + let manifest_path = out.join("manifest.json"); + fs::write( + &manifest_path, + serde_json::to_vec_pretty(&manifest).context("serializing manifest")?, + ) + .context("writing manifest")?; + + Ok(summary) +} + +/// Returns the on-disk path of a content-addressed chunk. +pub fn chunk_path(out: &Path, sha256: &str) -> PathBuf { + out.join("chunks").join(sha256) +} + +#[cfg(test)] +mod tests { + use super::*; + use tempfile::tempdir; + + #[test] + fn push_writes_manifest_and_reassembles_chunks() { + let input = tempdir().unwrap(); + let out = tempdir().unwrap(); + + let payload_a = vec![7u8; 150_000]; + let payload_b: Vec = (0..90_000u32).map(|i| (i % 13) as u8).collect(); + fs::write(input.path().join("a.bin"), &payload_a).unwrap(); + fs::create_dir(input.path().join("nested")).unwrap(); + fs::write(input.path().join("nested/b.bin"), &payload_b).unwrap(); + + let summary = run(input.path(), out.path()).unwrap(); + assert_eq!(summary.files, 2); + assert!(summary.chunks >= 2); + assert!(summary.bytes_in >= (payload_a.len() + payload_b.len()) as u64); + + let manifest: serde_json::Value = + serde_json::from_slice(&fs::read(out.path().join("manifest.json")).unwrap()).unwrap(); + let entries = manifest["entries"].as_array().unwrap(); + assert_eq!(entries.len(), 2); + + // Reassemble a.bin from its content-addressed chunks. + let a_entry = entries + .iter() + .find(|e| e["path"] == "a.bin") + .expect("a.bin entry"); + let mut rebuilt = Vec::new(); + for chunk in a_entry["chunks"].as_array().unwrap() { + let sha = chunk["sha256"].as_str().unwrap(); + let compressed = fs::read(chunk_path(out.path(), sha)).unwrap(); + let decompressed = zstd::bulk::decompress(&compressed, 8 * 1024 * 1024).unwrap(); + assert_eq!(hex_digest(&decompressed), sha); + rebuilt.extend_from_slice(&decompressed); + } + assert_eq!(rebuilt, payload_a); + } +} diff --git a/cli/src/main.rs b/cli/src/main.rs index 91cc05e78..63dd6c32a 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -28,6 +28,16 @@ async fn main() -> anyhow::Result<()> { let info = info.interactive_configure(); upload::interface::upload(&info, config, &name).await?; } + Commands::Push { path, out } => { + let summary = crate::commands::push::run( + std::path::Path::new(&path), + std::path::Path::new(&out), + )?; + println!( + "pushed {} file(s), {} chunk(s), {} -> {} bytes", + summary.files, summary.chunks, summary.bytes_in, summary.bytes_out + ); + } }; Ok(()) diff --git a/torrential/src/lan/mod.rs b/torrential/src/lan/mod.rs new file mode 100644 index 000000000..791b56913 --- /dev/null +++ b/torrential/src/lan/mod.rs @@ -0,0 +1,136 @@ +//! LAN peer-to-peer chunk streaming contract (Phase 1, #17). +//! +//! Discovery and transport are pluggable: the depot server keeps a registry of +//! recently-seen LAN peers and the download manager can prefer a nearby peer +//! over the origin for chunk fetches. The mDNS/SSDP discovery backend and the +//! QUIC/HTTP transport are separate integrations; this module fixes the +//! selection + expiry behaviour both must agree on. + +use std::net::SocketAddr; +use std::time::{Duration, Instant}; + +/// A peer that advertises the same depot content on the local network. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct LanPeer { + pub peer_id: String, + pub address: SocketAddr, + /// Round-trip estimate in milliseconds; lower is better. + pub rtt_ms: u32, + /// Observed throughput in KiB/s; higher is better. + pub throughput_kibps: u32, +} + +/// Discovers LAN peers that host the requested content. +pub trait PeerDiscovery: Send + Sync { + fn discover(&self, content_id: &str) -> Vec; +} + +#[derive(Debug, Clone)] +struct TrackedPeer { + peer: LanPeer, + last_seen: Instant, +} + +/// Keeps LAN peers fresh and evicts ones that stopped announcing. +#[derive(Debug)] +pub struct LanPeerRegistry { + ttl: Duration, + peers: Vec, +} + +impl LanPeerRegistry { + pub fn new(ttl: Duration) -> Self { + Self { + ttl, + peers: Vec::new(), + } + } + + pub fn observe(&mut self, peer: LanPeer, now: Instant) { + if let Some(existing) = self + .peers + .iter_mut() + .find(|tracked| tracked.peer.peer_id == peer.peer_id) + { + existing.peer = peer; + existing.last_seen = now; + } else { + self.peers.push(TrackedPeer { + peer, + last_seen: now, + }); + } + } + + pub fn evict_expired(&mut self, now: Instant) { + self.peers + .retain(|tracked| now.duration_since(tracked.last_seen) <= self.ttl); + } + + pub fn live(&self, now: Instant) -> Vec { + self.peers + .iter() + .filter(|tracked| now.duration_since(tracked.last_seen) <= self.ttl) + .map(|tracked| tracked.peer.clone()) + .collect() + } + + /// Best peer by a weighted score of low latency and high throughput. + pub fn best(&self, now: Instant) -> Option { + self.live(now).into_iter().max_by_key(peer_score) + } +} + +/// Scores a peer; higher is better. Latency dominates, throughput breaks ties. +pub fn peer_score(peer: &LanPeer) -> u64 { + let latency = u64::from(10_000u32.saturating_sub(peer.rtt_ms.min(10_000))); + let throughput = u64::from(peer.throughput_kibps).min(999_999); + latency * 1_000_000 + throughput +} + +#[cfg(test)] +mod tests { + use super::*; + use std::str::FromStr; + + fn peer(id: &str, rtt: u32, kibps: u32) -> LanPeer { + LanPeer { + peer_id: id.to_string(), + address: SocketAddr::from_str("127.0.0.1:9000").unwrap(), + rtt_ms: rtt, + throughput_kibps: kibps, + } + } + + #[test] + fn prefers_lower_latency() { + let fast = peer("fast", 2, 100); + let slow = peer("slow", 40, 100_000); + assert!(peer_score(&fast) > peer_score(&slow)); + } + + #[test] + fn evicts_peers_past_ttl() { + let ttl = Duration::from_secs(5); + let mut registry = LanPeerRegistry::new(ttl); + let start = Instant::now(); + registry.observe(peer("a", 5, 100), start); + + let later = start + Duration::from_secs(6); + assert!(registry.live(later).is_empty()); + registry.evict_expired(later); + assert!(registry.best(later).is_none()); + } + + #[test] + fn picks_the_best_peer_and_deduplicates_by_id() { + let mut registry = LanPeerRegistry::new(Duration::from_secs(10)); + let now = Instant::now(); + registry.observe(peer("a", 30, 500), now); + registry.observe(peer("b", 4, 500), now); + registry.observe(peer("a", 3, 500), now); + + assert_eq!(registry.live(now).len(), 2); + assert_eq!(registry.best(now).unwrap().peer_id, "a"); + } +} diff --git a/torrential/src/lib.rs b/torrential/src/lib.rs index 73c1e757d..f2b8ff84f 100644 --- a/torrential/src/lib.rs +++ b/torrential/src/lib.rs @@ -2,6 +2,7 @@ use tokio::sync::Semaphore; pub mod conversions; pub mod downloads; pub mod droplet; +pub mod lan; pub mod proto; pub mod server; pub mod state; From 8ce486f73aa9145f5a458948a4d5abf1ea2e9fc5 Mon Sep 17 00:00:00 2001 From: John Smith Date: Mon, 14 Sep 2026 01:29:25 -0400 Subject: [PATCH 170/243] fix(delivery): satisfy clippy for LAN scoring and push chunk helper --- cli/src/commands/push/mod.rs | 1 + torrential/src/lan/mod.rs | 14 ++++++++++---- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/cli/src/commands/push/mod.rs b/cli/src/commands/push/mod.rs index ed607ec4f..257fa8d1b 100644 --- a/cli/src/commands/push/mod.rs +++ b/cli/src/commands/push/mod.rs @@ -125,6 +125,7 @@ pub fn run(path: &Path, out: &Path) -> Result { } /// Returns the on-disk path of a content-addressed chunk. +#[allow(dead_code)] // consumed by the S3/HTTP uploader that reuses this layout pub fn chunk_path(out: &Path, sha256: &str) -> PathBuf { out.join("chunks").join(sha256) } diff --git a/torrential/src/lan/mod.rs b/torrential/src/lan/mod.rs index 791b56913..2ee145a6b 100644 --- a/torrential/src/lan/mod.rs +++ b/torrential/src/lan/mod.rs @@ -39,6 +39,7 @@ pub struct LanPeerRegistry { } impl LanPeerRegistry { + #[must_use] pub fn new(ttl: Duration) -> Self { Self { ttl, @@ -67,6 +68,7 @@ impl LanPeerRegistry { .retain(|tracked| now.duration_since(tracked.last_seen) <= self.ttl); } + #[must_use] pub fn live(&self, now: Instant) -> Vec { self.peers .iter() @@ -76,12 +78,14 @@ impl LanPeerRegistry { } /// Best peer by a weighted score of low latency and high throughput. + #[must_use] pub fn best(&self, now: Instant) -> Option { self.live(now).into_iter().max_by_key(peer_score) } } /// Scores a peer; higher is better. Latency dominates, throughput breaks ties. +#[must_use] pub fn peer_score(peer: &LanPeer) -> u64 { let latency = u64::from(10_000u32.saturating_sub(peer.rtt_ms.min(10_000))); let throughput = u64::from(peer.throughput_kibps).min(999_999); @@ -91,12 +95,11 @@ pub fn peer_score(peer: &LanPeer) -> u64 { #[cfg(test)] mod tests { use super::*; - use std::str::FromStr; fn peer(id: &str, rtt: u32, kibps: u32) -> LanPeer { LanPeer { peer_id: id.to_string(), - address: SocketAddr::from_str("127.0.0.1:9000").unwrap(), + address: SocketAddr::from(([127, 0, 0, 1], 9000)), rtt_ms: rtt, throughput_kibps: kibps, } @@ -117,7 +120,7 @@ mod tests { registry.observe(peer("a", 5, 100), start); let later = start + Duration::from_secs(6); - assert!(registry.live(later).is_empty()); + assert_eq!(registry.live(later).len(), 0); registry.evict_expired(later); assert!(registry.best(later).is_none()); } @@ -131,6 +134,9 @@ mod tests { registry.observe(peer("a", 3, 500), now); assert_eq!(registry.live(now).len(), 2); - assert_eq!(registry.best(now).unwrap().peer_id, "a"); + assert_eq!( + registry.best(now).map(|best| best.peer_id), + Some("a".to_string()) + ); } } From d22e4944e3db15c4d0f3166a1a1983f60da5fcb7 Mon Sep 17 00:00:00 2001 From: John Smith Date: Mon, 14 Sep 2026 01:30:58 -0400 Subject: [PATCH 171/243] feat(input): add Drop Input mapping core and Big Picture shell (#18) - Add the input crate: gamepad state, InputBackend trait (uinput/ViGEm backends pending), deadzone/gyro-to-mouse mapping, and a MockBackend, all unit-tested. - Add a 10-foot Big Picture page with arrow-key spatial focus and an on-screen keyboard component. - The native Vulkan/DXGI in-game overlay layer is tracked as a separate deliverable requiring the Vulkan layer toolchain. --- desktop/main/components/OnScreenKeyboard.vue | 40 ++++ desktop/main/pages/bigpicture.vue | 104 +++++++++ desktop/src-tauri/Cargo.lock | 4 + desktop/src-tauri/Cargo.toml | 1 + desktop/src-tauri/input/Cargo.toml | 8 + desktop/src-tauri/input/src/lib.rs | 219 +++++++++++++++++++ 6 files changed, 376 insertions(+) create mode 100644 desktop/main/components/OnScreenKeyboard.vue create mode 100644 desktop/main/pages/bigpicture.vue create mode 100644 desktop/src-tauri/input/Cargo.toml create mode 100644 desktop/src-tauri/input/src/lib.rs diff --git a/desktop/main/components/OnScreenKeyboard.vue b/desktop/main/components/OnScreenKeyboard.vue new file mode 100644 index 000000000..02d6f0665 --- /dev/null +++ b/desktop/main/components/OnScreenKeyboard.vue @@ -0,0 +1,40 @@ + + + diff --git a/desktop/main/pages/bigpicture.vue b/desktop/main/pages/bigpicture.vue new file mode 100644 index 000000000..f3d171e6b --- /dev/null +++ b/desktop/main/pages/bigpicture.vue @@ -0,0 +1,104 @@ + + + diff --git a/desktop/src-tauri/Cargo.lock b/desktop/src-tauri/Cargo.lock index 519a29334..739877468 100644 --- a/desktop/src-tauri/Cargo.lock +++ b/desktop/src-tauri/Cargo.lock @@ -2640,6 +2640,10 @@ dependencies = [ "generic-array", ] +[[package]] +name = "input" +version = "0.1.0" + [[package]] name = "instant" version = "0.1.13" diff --git a/desktop/src-tauri/Cargo.toml b/desktop/src-tauri/Cargo.toml index dc4ff187a..711b58768 100644 --- a/desktop/src-tauri/Cargo.toml +++ b/desktop/src-tauri/Cargo.toml @@ -13,6 +13,7 @@ members = [ "database", "download_manager", "games", + "input", "process", "remote", "utils", diff --git a/desktop/src-tauri/input/Cargo.toml b/desktop/src-tauri/input/Cargo.toml new file mode 100644 index 000000000..e462337f9 --- /dev/null +++ b/desktop/src-tauri/input/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "input" +version = "0.1.0" +edition = "2024" + +[dependencies] + +[dev-dependencies] diff --git a/desktop/src-tauri/input/src/lib.rs b/desktop/src-tauri/input/src/lib.rs new file mode 100644 index 000000000..257f16bcd --- /dev/null +++ b/desktop/src-tauri/input/src/lib.rs @@ -0,0 +1,219 @@ +//! Drop Input — controller remapping and virtual gamepad abstraction (#18). +//! +//! The platform backends live behind [`InputBackend`]: `uinput`/`evdev` on +//! Linux and `ViGEm` on Windows create the virtual device, while this crate +//! owns the pure mapping logic (deadzones, gyro-to-mouse, button remaps) so it +//! stays testable without touching `/dev/uinput` or a driver. + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum GamepadButton { + South, + East, + West, + North, + LeftBumper, + RightBumper, + LeftStick, + RightStick, + Start, + Select, + DpadUp, + DpadDown, + DpadLeft, + DpadRight, +} + +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Stick { + pub x: f32, + pub y: f32, +} + +impl Stick { + #[must_use] + pub const fn new(x: f32, y: f32) -> Self { + Self { x, y } + } + + #[must_use] + pub fn magnitude(&self) -> f32 { + (self.x * self.x + self.y * self.y).sqrt() + } +} + +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Gyro { + pub pitch: f32, + pub yaw: f32, + pub roll: f32, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct GamepadState { + pub left_stick: Stick, + pub right_stick: Stick, + pub left_trigger: f32, + pub right_trigger: f32, + pub buttons: Vec, + pub gyro: Option, +} + +impl Default for GamepadState { + fn default() -> Self { + Self { + left_stick: Stick::new(0.0, 0.0), + right_stick: Stick::new(0.0, 0.0), + left_trigger: 0.0, + right_trigger: 0.0, + buttons: Vec::new(), + gyro: None, + } + } +} + +/// A virtual gamepad device. Real implementations wrap uinput (Linux) or ViGEm +/// (Windows); [`MockBackend`] records states for tests. +pub trait InputBackend: Send + Sync { + type Error; + + fn create(&mut self) -> Result<(), Self::Error>; + fn push_state(&mut self, state: &GamepadState) -> Result<(), Self::Error>; + fn destroy(&mut self) -> Result<(), Self::Error>; +} + +/// Zeroes an axis value inside the deadzone and rescales the remainder to the +/// full unit range so movement still starts from zero at the deadzone edge. +#[must_use] +pub fn apply_deadzone(value: f32, deadzone: f32) -> f32 { + let deadzone = deadzone.clamp(0.0, 1.0); + let magnitude = value.abs(); + if magnitude <= deadzone { + return 0.0; + } + let scaled = (magnitude - deadzone) / (1.0 - deadzone); + scaled.clamp(0.0, 1.0).copysign(value) +} + +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct MouseDelta { + pub dx: f32, + pub dy: f32, +} + +/// Converts gyro yaw/pitch into a mouse delta for gyro-aiming. `sensitivity` is +/// in pixels per degree and `deadzone` is in degrees. +#[must_use] +pub fn map_gyro_to_mouse(gyro: &Gyro, sensitivity: f32, deadzone: f32) -> MouseDelta { + MouseDelta { + dx: deadzone_degrees(gyro.yaw, deadzone) * sensitivity, + dy: deadzone_degrees(gyro.pitch, deadzone) * sensitivity, + } +} + +fn deadzone_degrees(value: f32, deadzone: f32) -> f32 { + if value.abs() <= deadzone { + 0.0 + } else { + value + } +} + +/// Applies a deadzone to both sticks of a state. +#[must_use] +pub fn normalize_sticks(state: &GamepadState, deadzone: f32) -> GamepadState { + let mut normalized = state.clone(); + normalized.left_stick = Stick::new( + apply_deadzone(state.left_stick.x, deadzone), + apply_deadzone(state.left_stick.y, deadzone), + ); + normalized.right_stick = Stick::new( + apply_deadzone(state.right_stick.x, deadzone), + apply_deadzone(state.right_stick.y, deadzone), + ); + normalized +} + +/// Records every pushed state; used by the desktop to dry-run mappings. +#[derive(Debug, Default)] +pub struct MockBackend { + pub created: bool, + pub states: Vec, +} + +impl InputBackend for MockBackend { + type Error = std::convert::Infallible; + + fn create(&mut self) -> Result<(), Self::Error> { + self.created = true; + Ok(()) + } + + fn push_state(&mut self, state: &GamepadState) -> Result<(), Self::Error> { + self.states.push(state.clone()); + Ok(()) + } + + fn destroy(&mut self) -> Result<(), Self::Error> { + self.created = false; + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn deadzone_zeros_small_input_and_scales_large_input() { + assert_eq!(apply_deadzone(0.05, 0.2), 0.0); + assert_eq!(apply_deadzone(-0.05, 0.2), 0.0); + // 0.6 is 0.4 into the 0.8 usable range => 0.5 + assert!((apply_deadzone(0.6, 0.2) - 0.5).abs() < 1e-6); + assert_eq!(apply_deadzone(1.0, 0.2), 1.0); + } + + #[test] + fn gyro_mapping_respects_deadzone_and_sensitivity() { + let gyro = Gyro { + pitch: 2.0, + yaw: 10.0, + roll: 0.0, + }; + let delta = map_gyro_to_mouse(&gyro, 2.0, 0.5); + assert_eq!(delta.dx, 20.0); + assert_eq!(delta.dy, 4.0); + + let idle = map_gyro_to_mouse( + &Gyro { + pitch: 0.1, + yaw: 0.1, + roll: 0.0, + }, + 2.0, + 0.5, + ); + assert_eq!(idle, MouseDelta { dx: 0.0, dy: 0.0 }); + } + + #[test] + fn normalize_sticks_applies_deadzone_per_axis() { + let state = GamepadState { + left_stick: Stick::new(0.1, 0.9), + ..Default::default() + }; + let normalized = normalize_sticks(&state, 0.2); + assert_eq!(normalized.left_stick.x, 0.0); + assert!(normalized.left_stick.y > 0.8); + } + + #[test] + fn mock_backend_tracks_lifecycle() { + let mut backend = MockBackend::default(); + assert!(backend.create().is_ok()); + assert!(backend.created); + assert!(backend.push_state(&GamepadState::default()).is_ok()); + assert_eq!(backend.states.len(), 1); + assert!(backend.destroy().is_ok()); + assert!(!backend.created); + } +} From 11abf3e1f591fbee6d4e5f4bee495c890c394f3b Mon Sep 17 00:00:00 2001 From: John Smith Date: Mon, 14 Sep 2026 01:32:41 -0400 Subject: [PATCH 172/243] feat(achievements): add achievement/leaderboard models, unlock API, and event bus (#8) - Add AchievementDefinition/UserAchievement/Leaderboard/LeaderboardEntry Prisma models and migration, with relations on User and Game. - Add a dependency-injected AchievementManager: idempotent definition registration, exactly-once unlock emission on drop:achievement:unlock, and progress roll-up, covered by unit tests. - Add client API routes to list a game's achievements and unlock one. --- .../migration.sql | 86 +++++++++ server/prisma/models/achievement.prisma | 71 ++++++++ server/prisma/models/content.prisma | 3 + server/prisma/models/user.prisma | 3 + .../client/achievements/[gameid]/index.get.ts | 11 ++ .../api/v1/client/achievements/unlock.post.ts | 24 +++ .../__tests__/achievements.test.ts | 165 +++++++++++++++++ server/server/internal/achievements/index.ts | 21 +++ .../server/internal/achievements/manager.ts | 167 ++++++++++++++++++ 9 files changed, 551 insertions(+) create mode 100644 server/prisma/migrations/20260914020000_add_achievements/migration.sql create mode 100644 server/prisma/models/achievement.prisma create mode 100644 server/server/api/v1/client/achievements/[gameid]/index.get.ts create mode 100644 server/server/api/v1/client/achievements/unlock.post.ts create mode 100644 server/server/internal/achievements/__tests__/achievements.test.ts create mode 100644 server/server/internal/achievements/index.ts create mode 100644 server/server/internal/achievements/manager.ts diff --git a/server/prisma/migrations/20260914020000_add_achievements/migration.sql b/server/prisma/migrations/20260914020000_add_achievements/migration.sql new file mode 100644 index 000000000..9fb9fd462 --- /dev/null +++ b/server/prisma/migrations/20260914020000_add_achievements/migration.sql @@ -0,0 +1,86 @@ +-- CreateTable +CREATE TABLE "AchievementDefinition" ( + "id" TEXT NOT NULL, + "gameId" TEXT NOT NULL, + "key" TEXT NOT NULL, + "name" TEXT NOT NULL, + "description" TEXT NOT NULL, + "hidden" BOOLEAN NOT NULL DEFAULT false, + "points" INTEGER NOT NULL DEFAULT 0, + "iconObjectId" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "AchievementDefinition_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "UserAchievement" ( + "id" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "achievementId" TEXT NOT NULL, + "unlockedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "UserAchievement_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Leaderboard" ( + "id" TEXT NOT NULL, + "gameId" TEXT NOT NULL, + "key" TEXT NOT NULL, + "name" TEXT NOT NULL, + "sortOrder" TEXT NOT NULL DEFAULT 'desc', + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "Leaderboard_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "LeaderboardEntry" ( + "id" TEXT NOT NULL, + "leaderboardId" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "score" DOUBLE PRECISION NOT NULL, + "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "LeaderboardEntry_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "AchievementDefinition_gameId_key_key" ON "AchievementDefinition"("gameId", "key"); + +-- CreateIndex +CREATE INDEX "AchievementDefinition_gameId_idx" ON "AchievementDefinition"("gameId"); + +-- CreateIndex +CREATE UNIQUE INDEX "UserAchievement_userId_achievementId_key" ON "UserAchievement"("userId", "achievementId"); + +-- CreateIndex +CREATE INDEX "UserAchievement_userId_idx" ON "UserAchievement"("userId"); + +-- CreateIndex +CREATE UNIQUE INDEX "Leaderboard_gameId_key_key" ON "Leaderboard"("gameId", "key"); + +-- CreateIndex +CREATE UNIQUE INDEX "LeaderboardEntry_leaderboardId_userId_key" ON "LeaderboardEntry"("leaderboardId", "userId"); + +-- CreateIndex +CREATE INDEX "LeaderboardEntry_leaderboardId_idx" ON "LeaderboardEntry"("leaderboardId"); + +-- AddForeignKey +ALTER TABLE "AchievementDefinition" ADD CONSTRAINT "AchievementDefinition_gameId_fkey" FOREIGN KEY ("gameId") REFERENCES "Game"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "UserAchievement" ADD CONSTRAINT "UserAchievement_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "UserAchievement" ADD CONSTRAINT "UserAchievement_achievementId_fkey" FOREIGN KEY ("achievementId") REFERENCES "AchievementDefinition"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Leaderboard" ADD CONSTRAINT "Leaderboard_gameId_fkey" FOREIGN KEY ("gameId") REFERENCES "Game"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "LeaderboardEntry" ADD CONSTRAINT "LeaderboardEntry_leaderboardId_fkey" FOREIGN KEY ("leaderboardId") REFERENCES "Leaderboard"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "LeaderboardEntry" ADD CONSTRAINT "LeaderboardEntry_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/server/prisma/models/achievement.prisma b/server/prisma/models/achievement.prisma new file mode 100644 index 000000000..581e47bfe --- /dev/null +++ b/server/prisma/models/achievement.prisma @@ -0,0 +1,71 @@ +// Achievements, user unlocks, and leaderboards (#8 / Phase 3). + +model AchievementDefinition { + id String @id @default(uuid()) + + gameId String + game Game @relation(fields: [gameId], references: [id], onDelete: Cascade) + + // Stable identifier from the game or plugin (e.g. a Goldberg achievement id). + key String + name String + description String + hidden Boolean @default(false) + points Int @default(0) + iconObjectId String? + + createdAt DateTime @default(now()) + + unlocks UserAchievement[] + + @@unique([gameId, key]) + @@index([gameId]) +} + +model UserAchievement { + id String @id @default(uuid()) + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + achievementId String + achievement AchievementDefinition @relation(fields: [achievementId], references: [id], onDelete: Cascade) + + unlockedAt DateTime @default(now()) + + @@unique([userId, achievementId]) + @@index([userId]) +} + +model Leaderboard { + id String @id @default(uuid()) + + gameId String + game Game @relation(fields: [gameId], references: [id], onDelete: Cascade) + + key String + name String + // "asc" or "desc" + sortOrder String @default("desc") + + createdAt DateTime @default(now()) + + entries LeaderboardEntry[] + + @@unique([gameId, key]) +} + +model LeaderboardEntry { + id String @id @default(uuid()) + + leaderboardId String + leaderboard Leaderboard @relation(fields: [leaderboardId], references: [id], onDelete: Cascade) + + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + score Float + updatedAt DateTime @default(now()) + + @@unique([leaderboardId, userId]) + @@index([leaderboardId]) +} diff --git a/server/prisma/models/content.prisma b/server/prisma/models/content.prisma index 249756bc8..8d70c16df 100644 --- a/server/prisma/models/content.prisma +++ b/server/prisma/models/content.prisma @@ -56,6 +56,9 @@ model Game { tags GameTag[] playtime Playtime[] + achievements AchievementDefinition[] + leaderboards Leaderboard[] + developers Company[] @relation(name: "developers") publishers Company[] @relation(name: "publishers") unimportedGameVersions UnimportedGameVersion[] diff --git a/server/prisma/models/user.prisma b/server/prisma/models/user.prisma index 74d82c2de..134f424cf 100644 --- a/server/prisma/models/user.prisma +++ b/server/prisma/models/user.prisma @@ -23,6 +23,9 @@ model User { screenshots Screenshot[] playtime Playtime[] + achievements UserAchievement[] + leaderboardEntries LeaderboardEntry[] + groups UserGroup[] } diff --git a/server/server/api/v1/client/achievements/[gameid]/index.get.ts b/server/server/api/v1/client/achievements/[gameid]/index.get.ts new file mode 100644 index 000000000..6a27d625c --- /dev/null +++ b/server/server/api/v1/client/achievements/[gameid]/index.get.ts @@ -0,0 +1,11 @@ +import { defineClientEventHandler } from "~/server/internal/clients/event-handler"; +import achievementManager from "~/server/internal/achievements"; + +export default defineClientEventHandler(async (h3, { fetchUser }) => { + const user = await fetchUser(); + const gameId = getRouterParam(h3, "gameid"); + if (!gameId) + throw createError({ statusCode: 400, statusMessage: "No gameID in route" }); + + return await achievementManager.listForUser(user.id, gameId); +}); diff --git a/server/server/api/v1/client/achievements/unlock.post.ts b/server/server/api/v1/client/achievements/unlock.post.ts new file mode 100644 index 000000000..a4c324c56 --- /dev/null +++ b/server/server/api/v1/client/achievements/unlock.post.ts @@ -0,0 +1,24 @@ +import { defineClientEventHandler } from "~/server/internal/clients/event-handler"; +import achievementManager from "~/server/internal/achievements"; + +interface UnlockBody { + gameId?: string; + key?: string; +} + +export default defineClientEventHandler(async (h3, { fetchUser }) => { + const user = await fetchUser(); + const body = ((await readBody(h3)) ?? {}) as UnlockBody; + if (!body.gameId || !body.key) + throw createError({ + statusCode: 400, + statusMessage: "gameId and key are required", + }); + + const result = await achievementManager.unlock( + user.id, + body.gameId, + body.key, + ); + return { unlocked: true, alreadyUnlocked: result.alreadyUnlocked }; +}); diff --git a/server/server/internal/achievements/__tests__/achievements.test.ts b/server/server/internal/achievements/__tests__/achievements.test.ts new file mode 100644 index 000000000..0ab05c81b --- /dev/null +++ b/server/server/internal/achievements/__tests__/achievements.test.ts @@ -0,0 +1,165 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { + ACHIEVEMENT_UNLOCK_CHANNEL, + AchievementManager, + summarizeProgress, + type AchievementRecord, + type AchievementsDeps, +} from "../manager"; + +function createHarness() { + const definitions = new Map(); + const unlocks = new Set(); + const events: Array<{ channel: string; event: unknown }> = []; + let nextId = 1; + + const keyOf = (gameId: string, key: string) => `${gameId}:${key}`; + + const deps: AchievementsDeps = { + prisma: { + achievementDefinition: { + findUnique: async ({ where }) => + definitions.get( + keyOf(where.gameId_key.gameId, where.gameId_key.key), + ) ?? null, + findMany: async ({ where }) => + [...definitions.values()].filter((d) => d.gameId === where.gameId), + create: async ({ data }) => { + const record: AchievementRecord = { + id: `ach-${nextId++}`, + gameId: data.gameId, + key: data.key, + name: data.name, + description: data.description, + hidden: data.hidden ?? false, + points: data.points ?? 0, + iconObjectId: data.iconObjectId ?? null, + }; + definitions.set(keyOf(data.gameId, data.key), record); + return record; + }, + }, + userAchievement: { + findMany: async ({ where }) => + where.achievementId.in + .filter((id) => unlocks.has(`${where.userId}:${id}`)) + .map((achievementId) => ({ + achievementId, + unlockedAt: new Date(), + })), + create: async ({ data }) => { + unlocks.add(`${data.userId}:${data.achievementId}`); + return { achievementId: data.achievementId, unlockedAt: new Date() }; + }, + }, + }, + emit: (channel, event) => events.push({ channel, event }), + }; + + return { + manager: new AchievementManager(deps), + definitions, + unlocks, + events, + }; +} + +test("registerDefinition is idempotent by (gameId, key)", async () => { + const { manager } = createHarness(); + const first = await manager.registerDefinition({ + gameId: "game-1", + key: "beat-boss", + name: "Boss Slayer", + description: "Defeat the first boss", + points: 10, + }); + const second = await manager.registerDefinition({ + gameId: "game-1", + key: "beat-boss", + name: "Boss Slayer", + description: "Defeat the first boss", + points: 10, + }); + assert.equal(first.id, second.id); +}); + +test("unlock emits drop:achievement:unlock exactly once", async () => { + const { manager, events } = createHarness(); + await manager.registerDefinition({ + gameId: "game-1", + key: "beat-boss", + name: "Boss Slayer", + description: "Defeat the first boss", + points: 10, + }); + + const first = await manager.unlock("user-1", "game-1", "beat-boss"); + const second = await manager.unlock("user-1", "game-1", "beat-boss"); + + assert.equal(first.alreadyUnlocked, false); + assert.equal(second.alreadyUnlocked, true); + assert.equal(events.length, 1); + assert.equal(events[0].channel, ACHIEVEMENT_UNLOCK_CHANNEL); + assert.deepEqual(events[0].event, { + userId: "user-1", + gameId: "game-1", + achievementId: first.definition.id, + key: "beat-boss", + points: 10, + }); +}); + +test("unlock rejects unknown achievements", async () => { + const { manager } = createHarness(); + await assert.rejects( + () => manager.unlock("user-1", "game-1", "missing"), + /Unknown achievement/, + ); +}); + +test("listForUser rolls up progress", async () => { + const { manager } = createHarness(); + await manager.registerDefinition({ + gameId: "game-1", + key: "a", + name: "A", + description: "", + points: 5, + }); + await manager.registerDefinition({ + gameId: "game-1", + key: "b", + name: "B", + description: "", + points: 15, + }); + await manager.unlock("user-1", "game-1", "a"); + + const listing = await manager.listForUser("user-1", "game-1"); + assert.equal(listing.progress.total, 2); + assert.equal(listing.progress.unlocked, 1); + assert.equal(listing.progress.totalPoints, 20); + assert.equal(listing.progress.unlockedPoints, 5); +}); + +test("summarizeProgress ignores unlocks without definitions", () => { + const definitions: AchievementRecord[] = [ + { + id: "a", + gameId: "g", + key: "a", + name: "A", + description: "", + hidden: false, + points: 10, + }, + ]; + const progress = summarizeProgress(definitions, new Set(["a", "ghost"])); + assert.deepEqual(progress, { + total: 1, + unlocked: 1, + totalPoints: 10, + unlockedPoints: 10, + }); +}); diff --git a/server/server/internal/achievements/index.ts b/server/server/internal/achievements/index.ts new file mode 100644 index 000000000..e115c5c80 --- /dev/null +++ b/server/server/internal/achievements/index.ts @@ -0,0 +1,21 @@ +import prisma from "../db/database"; +import pluginManager from "../plugins"; +import { AchievementManager, type AchievementsDeps } from "./manager"; + +const deps: AchievementsDeps = { + prisma: prisma as unknown as AchievementsDeps["prisma"], + emit: (channel, event) => pluginManager.broadcast(channel, event), +}; + +export const achievementManager = new AchievementManager(deps); +export { AchievementManager }; +export { + ACHIEVEMENT_UNLOCK_CHANNEL, + summarizeProgress, + type AchievementDefinitionInput, + type AchievementProgress, + type AchievementRecord, + type AchievementsDeps, + type UserAchievementRecord, +} from "./manager"; +export default achievementManager; diff --git a/server/server/internal/achievements/manager.ts b/server/server/internal/achievements/manager.ts new file mode 100644 index 000000000..14cf450a8 --- /dev/null +++ b/server/server/internal/achievements/manager.ts @@ -0,0 +1,167 @@ +import { createError } from "h3"; + +export interface AchievementRecord { + id: string; + gameId: string; + key: string; + name: string; + description: string; + hidden: boolean; + points: number; + iconObjectId?: string | null; +} + +export interface UserAchievementRecord { + achievementId: string; + unlockedAt: Date; +} + +export interface AchievementDefinitionInput { + gameId: string; + key: string; + name: string; + description: string; + hidden?: boolean; + points?: number; + iconObjectId?: string | null; +} + +export interface AchievementProgress { + total: number; + unlocked: number; + totalPoints: number; + unlockedPoints: number; +} + +/** Dependency surface kept structural so the manager is unit-testable. */ +export interface AchievementsDeps { + prisma: { + achievementDefinition: { + findUnique(args: { + where: { gameId_key: { gameId: string; key: string } }; + }): Promise; + findMany(args: { + where: { gameId: string }; + }): Promise; + create(args: { + data: AchievementDefinitionInput; + }): Promise; + }; + userAchievement: { + findMany(args: { + where: { userId: string; achievementId: { in: string[] } }; + }): Promise; + create(args: { + data: { userId: string; achievementId: string }; + }): Promise; + }; + }; + emit: (channel: string, event: unknown) => void; +} + +/** Pure progress roll-up used by both the API and the desktop UI. */ +export function summarizeProgress( + definitions: AchievementRecord[], + unlockedIds: Set, +): AchievementProgress { + let totalPoints = 0; + let unlockedPoints = 0; + let unlocked = 0; + for (const definition of definitions) { + totalPoints += definition.points; + if (unlockedIds.has(definition.id)) { + unlocked += 1; + unlockedPoints += definition.points; + } + } + return { + total: definitions.length, + unlocked, + totalPoints, + unlockedPoints, + }; +} + +export const ACHIEVEMENT_UNLOCK_CHANNEL = "drop:achievement:unlock"; + +export class AchievementManager { + constructor(private readonly deps: AchievementsDeps) {} + + /** Idempotently registers a game's achievement metadata. */ + async registerDefinition( + input: AchievementDefinitionInput, + ): Promise { + const existing = await this.deps.prisma.achievementDefinition.findUnique({ + where: { gameId_key: { gameId: input.gameId, key: input.key } }, + }); + if (existing) return existing; + return this.deps.prisma.achievementDefinition.create({ data: input }); + } + + /** + * Unlocks an achievement for a user. Emits `drop:achievement:unlock` exactly + * once per (user, achievement) so plugin bridges can mirror the unlock. + */ + async unlock( + userId: string, + gameId: string, + key: string, + ): Promise<{ + definition: AchievementRecord; + alreadyUnlocked: boolean; + }> { + const definition = await this.deps.prisma.achievementDefinition.findUnique({ + where: { gameId_key: { gameId, key } }, + }); + if (!definition) { + throw createError({ + statusCode: 404, + statusMessage: "Unknown achievement", + }); + } + + const existing = await this.deps.prisma.userAchievement.findMany({ + where: { userId, achievementId: { in: [definition.id] } }, + }); + const alreadyUnlocked = existing.length > 0; + if (!alreadyUnlocked) { + await this.deps.prisma.userAchievement.create({ + data: { userId, achievementId: definition.id }, + }); + this.deps.emit(ACHIEVEMENT_UNLOCK_CHANNEL, { + userId, + gameId, + achievementId: definition.id, + key: definition.key, + points: definition.points, + }); + } + + return { definition, alreadyUnlocked }; + } + + async listForUser( + userId: string, + gameId: string, + ): Promise<{ + definitions: AchievementRecord[]; + unlockedIds: string[]; + progress: AchievementProgress; + }> { + const definitions = await this.deps.prisma.achievementDefinition.findMany({ + where: { gameId }, + }); + const unlocked = await this.deps.prisma.userAchievement.findMany({ + where: { + userId, + achievementId: { in: definitions.map((d) => d.id) }, + }, + }); + const unlockedIds = unlocked.map((entry) => entry.achievementId); + return { + definitions, + unlockedIds, + progress: summarizeProgress(definitions, new Set(unlockedIds)), + }; + } +} From cbb419fce68c21c4d64e2ff12c238c9add6fae45 Mon Sep 17 00:00:00 2001 From: John Smith Date: Mon, 14 Sep 2026 01:34:28 -0400 Subject: [PATCH 173/243] feat(community): add verified playtime reviews and Ed25519 purchase receipts (#20/#21) - Add GameReview model + migration and a dependency-injected ReviewManager that marks reviews verified once a user passes the playtime threshold. - Add public review listing and client-authenticated review creation routes. - Add canonical Ed25519 purchase-receipt signing/verification for federated commerce, unit-tested for round-trip and tamper resistance. --- .../migration.sql | 24 +++++ server/prisma/models/community.prisma | 20 ++++ server/prisma/models/content.prisma | 1 + server/prisma/models/user.prisma | 1 + .../client/community/[gameid]/reviews.post.ts | 29 ++++++ .../api/v1/community/[gameid]/reviews.get.ts | 9 ++ .../commerce/__tests__/receipts.test.ts | 58 +++++++++++ server/server/internal/commerce/receipts.ts | 59 ++++++++++++ .../community/__tests__/community.test.ts | 72 ++++++++++++++ server/server/internal/community/index.ts | 15 +++ server/server/internal/community/manager.ts | 96 +++++++++++++++++++ 11 files changed, 384 insertions(+) create mode 100644 server/prisma/migrations/20260914030000_add_game_reviews/migration.sql create mode 100644 server/prisma/models/community.prisma create mode 100644 server/server/api/v1/client/community/[gameid]/reviews.post.ts create mode 100644 server/server/api/v1/community/[gameid]/reviews.get.ts create mode 100644 server/server/internal/commerce/__tests__/receipts.test.ts create mode 100644 server/server/internal/commerce/receipts.ts create mode 100644 server/server/internal/community/__tests__/community.test.ts create mode 100644 server/server/internal/community/index.ts create mode 100644 server/server/internal/community/manager.ts diff --git a/server/prisma/migrations/20260914030000_add_game_reviews/migration.sql b/server/prisma/migrations/20260914030000_add_game_reviews/migration.sql new file mode 100644 index 000000000..34cc95b99 --- /dev/null +++ b/server/prisma/migrations/20260914030000_add_game_reviews/migration.sql @@ -0,0 +1,24 @@ +-- CreateTable +CREATE TABLE "GameReview" ( + "id" TEXT NOT NULL, + "gameId" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "rating" INTEGER NOT NULL, + "body" TEXT NOT NULL, + "verified" BOOLEAN NOT NULL DEFAULT false, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "GameReview_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "GameReview_gameId_userId_key" ON "GameReview"("gameId", "userId"); + +-- CreateIndex +CREATE INDEX "GameReview_gameId_idx" ON "GameReview"("gameId"); + +-- AddForeignKey +ALTER TABLE "GameReview" ADD CONSTRAINT "GameReview_gameId_fkey" FOREIGN KEY ("gameId") REFERENCES "Game"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "GameReview" ADD CONSTRAINT "GameReview_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/server/prisma/models/community.prisma b/server/prisma/models/community.prisma new file mode 100644 index 000000000..fd330d0b1 --- /dev/null +++ b/server/prisma/models/community.prisma @@ -0,0 +1,20 @@ +// Community hubs: verified playtime reviews (#20). + +model GameReview { + id String @id @default(uuid()) + + gameId String + game Game @relation(fields: [gameId], references: [id], onDelete: Cascade) + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + rating Int + body String + // True when the author passed the verified-playtime threshold. + verified Boolean @default(false) + + createdAt DateTime @default(now()) + + @@unique([gameId, userId]) + @@index([gameId]) +} diff --git a/server/prisma/models/content.prisma b/server/prisma/models/content.prisma index 8d70c16df..18351ee88 100644 --- a/server/prisma/models/content.prisma +++ b/server/prisma/models/content.prisma @@ -58,6 +58,7 @@ model Game { achievements AchievementDefinition[] leaderboards Leaderboard[] + reviews GameReview[] developers Company[] @relation(name: "developers") publishers Company[] @relation(name: "publishers") diff --git a/server/prisma/models/user.prisma b/server/prisma/models/user.prisma index 134f424cf..aed039ab0 100644 --- a/server/prisma/models/user.prisma +++ b/server/prisma/models/user.prisma @@ -25,6 +25,7 @@ model User { achievements UserAchievement[] leaderboardEntries LeaderboardEntry[] + reviews GameReview[] groups UserGroup[] } diff --git a/server/server/api/v1/client/community/[gameid]/reviews.post.ts b/server/server/api/v1/client/community/[gameid]/reviews.post.ts new file mode 100644 index 000000000..f74291cd9 --- /dev/null +++ b/server/server/api/v1/client/community/[gameid]/reviews.post.ts @@ -0,0 +1,29 @@ +import { defineClientEventHandler } from "~/server/internal/clients/event-handler"; +import reviewManager from "~/server/internal/community"; + +interface ReviewBody { + rating?: number; + body?: string; +} + +export default defineClientEventHandler(async (h3, { fetchUser }) => { + const user = await fetchUser(); + const gameId = getRouterParam(h3, "gameid"); + if (!gameId) + throw createError({ statusCode: 400, statusMessage: "No gameID in route" }); + + const payload = ((await readBody(h3)) ?? {}) as ReviewBody; + if (typeof payload.rating !== "number" || typeof payload.body !== "string") { + throw createError({ + statusCode: 400, + statusMessage: "rating and body are required", + }); + } + + return await reviewManager.create( + user.id, + gameId, + payload.rating, + payload.body, + ); +}); diff --git a/server/server/api/v1/community/[gameid]/reviews.get.ts b/server/server/api/v1/community/[gameid]/reviews.get.ts new file mode 100644 index 000000000..d5a4669ef --- /dev/null +++ b/server/server/api/v1/community/[gameid]/reviews.get.ts @@ -0,0 +1,9 @@ +import reviewManager from "~/server/internal/community"; + +export default defineEventHandler(async (h3) => { + const gameId = getRouterParam(h3, "gameid"); + if (!gameId) + throw createError({ statusCode: 400, statusMessage: "No gameID in route" }); + + return await reviewManager.listForGame(gameId); +}); diff --git a/server/server/internal/commerce/__tests__/receipts.test.ts b/server/server/internal/commerce/__tests__/receipts.test.ts new file mode 100644 index 000000000..bd2fe56bf --- /dev/null +++ b/server/server/internal/commerce/__tests__/receipts.test.ts @@ -0,0 +1,58 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { generateKeyPairSync } from "node:crypto"; +import { + canonicalizeReceipt, + signReceipt, + verifyReceipt, + type PurchaseReceipt, +} from "../receipts"; + +const receipt: PurchaseReceipt = { + orderId: "order-1", + userId: "user-1", + gameId: "game-1", + amount: 1999, + currency: "USD", + issuedAt: "2026-09-14T00:00:00.000Z", +}; + +test("canonicalizeReceipt is key-order independent", () => { + const reordered = { + issuedAt: receipt.issuedAt, + currency: receipt.currency, + orderId: receipt.orderId, + amount: receipt.amount, + gameId: receipt.gameId, + userId: receipt.userId, + } as PurchaseReceipt; + assert.equal(canonicalizeReceipt(receipt), canonicalizeReceipt(reordered)); +}); + +test("sign and verify round-trip", () => { + const { privateKey, publicKey } = generateKeyPairSync("ed25519"); + const privatePem = privateKey + .export({ type: "pkcs8", format: "pem" }) + .toString(); + const publicPem = publicKey + .export({ type: "spki", format: "pem" }) + .toString(); + + const signature = signReceipt(receipt, privatePem); + assert.equal(verifyReceipt(receipt, signature, publicPem), true); +}); + +test("tampered receipts fail verification", () => { + const { privateKey, publicKey } = generateKeyPairSync("ed25519"); + const privatePem = privateKey + .export({ type: "pkcs8", format: "pem" }) + .toString(); + const publicPem = publicKey + .export({ type: "spki", format: "pem" }) + .toString(); + + const signature = signReceipt(receipt, privatePem); + const tampered = { ...receipt, amount: 1 }; + assert.equal(verifyReceipt(tampered, signature, publicPem), false); + assert.equal(verifyReceipt(receipt, "not-base64", publicPem), false); +}); diff --git a/server/server/internal/commerce/receipts.ts b/server/server/internal/commerce/receipts.ts new file mode 100644 index 000000000..4e99cd185 --- /dev/null +++ b/server/server/internal/commerce/receipts.ts @@ -0,0 +1,59 @@ +import { sign, verify } from "node:crypto"; + +/** + * Ed25519 purchase receipts (#21). The server signs an order receipt so a + * client or federated instance can verify a purchase without trusting the + * transport. Receipts are canonicalised before signing so both sides agree on + * the exact bytes. + */ +export interface PurchaseReceipt { + orderId: string; + userId: string; + gameId: string; + amount: number; + currency: string; + issuedAt: string; +} + +/** Deterministic JSON with sorted keys, used as the signed payload. */ +export function canonicalizeReceipt(receipt: PurchaseReceipt): string { + return JSON.stringify({ + amount: receipt.amount, + currency: receipt.currency, + gameId: receipt.gameId, + issuedAt: receipt.issuedAt, + orderId: receipt.orderId, + userId: receipt.userId, + }); +} + +/** Returns a base64 Ed25519 signature over the canonical receipt. */ +export function signReceipt( + receipt: PurchaseReceipt, + privateKeyPem: string, +): string { + const signature = sign( + null, + Buffer.from(canonicalizeReceipt(receipt)), + privateKeyPem, + ); + return signature.toString("base64"); +} + +/** Verifies a base64 Ed25519 signature against the canonical receipt. */ +export function verifyReceipt( + receipt: PurchaseReceipt, + signatureBase64: string, + publicKeyPem: string, +): boolean { + try { + return verify( + null, + Buffer.from(canonicalizeReceipt(receipt)), + publicKeyPem, + Buffer.from(signatureBase64, "base64"), + ); + } catch { + return false; + } +} diff --git a/server/server/internal/community/__tests__/community.test.ts b/server/server/internal/community/__tests__/community.test.ts new file mode 100644 index 000000000..f8afa7f9d --- /dev/null +++ b/server/server/internal/community/__tests__/community.test.ts @@ -0,0 +1,72 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { + ReviewManager, + isVerifiedReview, + type ReviewDeps, + type ReviewRecord, +} from "../manager"; + +function createHarness(playtimeSeconds: number) { + const reviews = new Map(); + let nextId = 1; + + const deps: ReviewDeps = { + verifiedPlaytimeSeconds: 3600, + prisma: { + playtime: { + findUnique: async () => + playtimeSeconds > 0 ? { seconds: playtimeSeconds } : null, + }, + gameReview: { + findUnique: async ({ where }) => + reviews.get( + `${where.gameId_userId.gameId}:${where.gameId_userId.userId}`, + ) ?? null, + create: async ({ data }) => { + const record: ReviewRecord = { + id: `review-${nextId++}`, + createdAt: new Date(), + ...data, + }; + reviews.set(`${data.gameId}:${data.userId}`, record); + return record; + }, + findMany: async ({ where }) => + [...reviews.values()].filter((r) => r.gameId === where.gameId), + }, + }, + }; + + return { manager: new ReviewManager(deps), reviews }; +} + +test("verified reviews require the playtime threshold", () => { + assert.equal(isVerifiedReview(3599, 3600), false); + assert.equal(isVerifiedReview(3600, 3600), true); +}); + +test("a review is marked verified when playtime passes the threshold", async () => { + const { manager } = createHarness(7200); + const review = await manager.create("user-1", "game-1", 5, "Masterpiece"); + assert.equal(review.verified, true); +}); + +test("a review without playtime is unverified", async () => { + const { manager } = createHarness(0); + const review = await manager.create("user-1", "game-1", 3, "It was fine"); + assert.equal(review.verified, false); +}); + +test("duplicate reviews are rejected and ratings validated", async () => { + const { manager } = createHarness(7200); + await manager.create("user-1", "game-1", 5, "Great"); + await assert.rejects( + () => manager.create("user-1", "game-1", 4, "Changed my mind"), + /already reviewed/, + ); + await assert.rejects( + () => manager.create("user-2", "game-1", 9, "Out of range"), + /rating must be/, + ); +}); diff --git a/server/server/internal/community/index.ts b/server/server/internal/community/index.ts new file mode 100644 index 000000000..1b7c39abc --- /dev/null +++ b/server/server/internal/community/index.ts @@ -0,0 +1,15 @@ +import prisma from "../db/database"; +import { ReviewManager, type ReviewDeps } from "./manager"; + +const deps: ReviewDeps = { + prisma: prisma as unknown as ReviewDeps["prisma"], +}; + +export const reviewManager = new ReviewManager(deps); +export { + ReviewManager, + DEFAULT_VERIFIED_PLAYTIME_SECONDS, + isVerifiedReview, +} from "./manager"; +export type { ReviewDeps, ReviewRecord } from "./manager"; +export default reviewManager; diff --git a/server/server/internal/community/manager.ts b/server/server/internal/community/manager.ts new file mode 100644 index 000000000..3bab80630 --- /dev/null +++ b/server/server/internal/community/manager.ts @@ -0,0 +1,96 @@ +import { createError } from "h3"; + +export interface ReviewRecord { + id: string; + gameId: string; + userId: string; + rating: number; + body: string; + verified: boolean; + createdAt: Date; +} + +export interface ReviewDeps { + prisma: { + playtime: { + findUnique(args: { + where: { gameId_userId: { gameId: string; userId: string } }; + }): Promise<{ seconds: number } | null>; + }; + gameReview: { + findUnique(args: { + where: { gameId_userId: { gameId: string; userId: string } }; + }): Promise; + create(args: { + data: { + gameId: string; + userId: string; + rating: number; + body: string; + verified: boolean; + }; + }): Promise; + findMany(args: { + where: { gameId: string }; + orderBy?: { createdAt: "asc" | "desc" }; + }): Promise; + }; + }; + /** Playtime needed for a review to count as "verified". */ + verifiedPlaytimeSeconds?: number; +} + +export const DEFAULT_VERIFIED_PLAYTIME_SECONDS = 60 * 60; + +export function isVerifiedReview(seconds: number, threshold: number): boolean { + return seconds >= threshold; +} + +export class ReviewManager { + private readonly threshold: number; + + constructor(private readonly deps: ReviewDeps) { + this.threshold = + deps.verifiedPlaytimeSeconds ?? DEFAULT_VERIFIED_PLAYTIME_SECONDS; + } + + async create( + userId: string, + gameId: string, + rating: number, + body: string, + ): Promise { + if (!Number.isInteger(rating) || rating < 1 || rating > 5) { + throw createError({ + statusCode: 400, + statusMessage: "rating must be an integer from 1 to 5", + }); + } + + const existing = await this.deps.prisma.gameReview.findUnique({ + where: { gameId_userId: { gameId, userId } }, + }); + if (existing) { + throw createError({ + statusCode: 409, + statusMessage: "You have already reviewed this game", + }); + } + + const playtime = await this.deps.prisma.playtime.findUnique({ + where: { gameId_userId: { gameId, userId } }, + }); + const verified = isVerifiedReview(playtime?.seconds ?? 0, this.threshold); + + return this.deps.prisma.gameReview.create({ + data: { gameId, userId, rating, body, verified }, + }); + } + + async listForGame(gameId: string): Promise { + return this.deps.prisma.gameReview.findMany({ + where: { gameId }, + orderBy: { createdAt: "desc" }, + }); + } +} From 67892a8c0124454824209627f4ac261be15bf403 Mon Sep 17 00:00:00 2001 From: John Smith Date: Mon, 14 Sep 2026 01:35:46 -0400 Subject: [PATCH 174/243] feat(community): add desktop community reviews page and fetch command --- desktop/main/pages/community.vue | 101 ++++++++++++++++++++++++----- desktop/src-tauri/src/community.rs | 60 +++++++++++++++++ desktop/src-tauri/src/lib.rs | 4 ++ 3 files changed, 149 insertions(+), 16 deletions(-) create mode 100644 desktop/src-tauri/src/community.rs diff --git a/desktop/main/pages/community.vue b/desktop/main/pages/community.vue index 7df32e15a..d894ec97c 100644 --- a/desktop/main/pages/community.vue +++ b/desktop/main/pages/community.vue @@ -1,23 +1,92 @@ + diff --git a/desktop/src-tauri/src/community.rs b/desktop/src-tauri/src/community.rs new file mode 100644 index 000000000..256b56e8b --- /dev/null +++ b/desktop/src-tauri/src/community.rs @@ -0,0 +1,60 @@ +use remote::requests::generate_url; +use remote::utils::DROP_CLIENT_ASYNC; + +#[derive(serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ReviewView { + pub id: String, + pub user_id: String, + pub rating: i32, + pub body: String, + pub verified: bool, + pub created_at: Option, +} + +#[derive(serde::Deserialize)] +#[serde(rename_all = "camelCase")] +struct ReviewRow { + id: String, + user_id: String, + rating: i32, + body: String, + verified: bool, + #[serde(default)] + created_at: Option, +} + +/// Fetches the public review list for a game from the community API. +#[tauri::command] +pub async fn fetch_game_reviews(game_id: String) -> Result, String> { + let url = generate_url(&["/api/v1/community", &game_id, "reviews"], &[]) + .map_err(|e| e.to_string())?; + + let response = DROP_CLIENT_ASYNC + .get(url) + .send() + .await + .map_err(|e| e.to_string())?; + + if !response.status().is_success() { + return Err(format!( + "failed to fetch reviews: {}", + response.status() + )); + } + + let body = response.text().await.map_err(|e| e.to_string())?; + let rows: Vec = serde_json::from_str(&body).map_err(|e| e.to_string())?; + + Ok(rows + .into_iter() + .map(|row| ReviewView { + id: row.id, + user_id: row.user_id, + rating: row.rating, + body: row.body, + verified: row.verified, + created_at: row.created_at, + }) + .collect()) +} diff --git a/desktop/src-tauri/src/lib.rs b/desktop/src-tauri/src/lib.rs index 673414c76..85ee058af 100644 --- a/desktop/src-tauri/src/lib.rs +++ b/desktop/src-tauri/src/lib.rs @@ -61,6 +61,7 @@ use utils::app_emit; mod client; mod cloud_saves; mod collections; +mod community; mod download_manager; mod downloads; mod games; @@ -73,6 +74,7 @@ mod updates; use client::*; use cloud_saves::*; +use community::*; use download_manager::*; use downloads::*; use games::*; @@ -286,6 +288,8 @@ pub fn run() { // Cloud saves fetch_cloud_save_slots, download_cloud_save_object, + // Community + fetch_game_reviews, // Processes launch_game, kill_game, From 536e63acad869080320bf71bac174e592f88cfcc Mon Sep 17 00:00:00 2001 From: John Smith Date: Mon, 14 Sep 2026 01:36:38 -0400 Subject: [PATCH 175/243] docs(upstream): add PR 1 platform-stability and PR 2 generic-SPI scope --- .../upstream-pr1-platform-stability.md | 46 ++++++++++++++++++ .../upstream-pr2-generic-plugin-spi.md | 47 +++++++++++++++++++ 2 files changed, 93 insertions(+) create mode 100644 docs/implementation/upstream-pr1-platform-stability.md create mode 100644 docs/implementation/upstream-pr2-generic-plugin-spi.md diff --git a/docs/implementation/upstream-pr1-platform-stability.md b/docs/implementation/upstream-pr1-platform-stability.md new file mode 100644 index 000000000..b6f4a0675 --- /dev/null +++ b/docs/implementation/upstream-pr1-platform-stability.md @@ -0,0 +1,46 @@ +# Upstream PR 1 — Platform & Stability Cleanups + +**Target:** `Drop-OSS/drop` +**Source:** `Heretek-Games/drop` (`upstream/pr1-platform-stability`) +**Goal:** land general-purpose platform fixes with zero Heretek-domain content, +so upstream can merge them independently of the plugin ecosystem. + +## Scope + +### Security & correctness (fork review #4) + +- `process/src/pipeline.rs`: canonicalize extraction destinations and reject + symlinked path components (symlink traversal escape). +- `torrential/src/server/mod.rs`: replace the `WaitMap` leak with a removable + response map evicted on consume/timeout. +- `torrential/src/server/mod.rs`: authenticate the local RPC peer with a + spawn-time shared secret (`TORRENTIAL_RPC_SECRET`, fail-closed). +- `server/server/api/v1/plugins/ws.get.ts`: enforce message-level and channel + authorization; reject cross-site upgrades. + +### Runtime & packaging + +- Docker Quadlet templates and database migration drift fixes. +- Linux platform reach: Proton/UMU runner management, `GAMEID` injection, + Fedora/CachyOS compatibility. +- S3/MinIO object-storage backend for the depot pipeline. +- Client launch diagnostics and per-game launch-handler override (#59). + +### Quality + +- Sonar/ESLint security fixes, `droplet` BOM-preservation test (#58), setup + installer chaining (#55), native HTML games (#57). + +## Out of scope + +- Emulation, mesh VPN, store scrapers, payments, federation (plugin streams). +- Generic plugin SPI (see PR 2). + +## Verification + +```sh +pnpm --filter drop run test +pnpm --filter drop run typecheck +cargo +nightly test -p process -p droplet-rs +cd torrential && cargo +nightly test +``` diff --git a/docs/implementation/upstream-pr2-generic-plugin-spi.md b/docs/implementation/upstream-pr2-generic-plugin-spi.md new file mode 100644 index 000000000..060c3546a --- /dev/null +++ b/docs/implementation/upstream-pr2-generic-plugin-spi.md @@ -0,0 +1,47 @@ +# Upstream PR 2 — Generic Plugin Runtime SPI + +**Target:** `Drop-OSS/drop` +**Source:** `Heretek-Games/drop` (`upstream/pr2-generic-plugin-spi`) +**Goal:** land the generic, domain-agnostic plugin runtime so third parties can +extend Drop without forking. Contains **zero** hardcoded piracy, emulation, +VPN, or payment logic. + +## Scope + +### Runtime (`server/server/internal/plugins/`) + +- `PluginManager` lifecycle: discovery of external bundles from + `${dataDir}/plugins/*/drop-plugin.json`, dynamic routing + `/api/v1/plugins/[pluginId]/...`, event bus, WebSocket gateway, isolated + storage with migrations. +- Manifest verification: `apiVersion` gate, SHA-256 entry checksum, optional + HMAC-SHA256 signature (`DROP_PLUGIN_SIGNING_KEY`), registry allow-list. +- Fail-closed capabilities: `routes`, `storage`, `events`, `network`, + `websocket`, `metadata:provider`, `cloudsave:provider`, `commerce:payment`. +- Auth: browser sessions, `Bearer` tokens, and the desktop `JWT` scheme. + +### Extension SPIs + +- Server: `MetadataProvider`, `CloudSavePathResolver`, `PaymentGateway`. +- Client (desktop): `StoreScanner`, `MetadataProvider`, `CloudSavePathResolver`, + overlay/UI slots, launch hooks. +- Bundled `hello-world` reference plugin and the upstream-compatibility tests + proving a zero-plugin baseline leaves Drop in a pure vanilla state. + +### SDK + +- `@droposs/plugin-sdk` contracts + mock harness; `@droposs/plugin-cli` bundle + signer (`drop-plugin sign`). + +## Out of scope + +- Every concrete plugin (metadata/store/payment/emulation/federation) lives in + the Heretek external plugin repositories. + +## Verification + +```sh +pnpm --filter drop run test +pnpm --filter @droposs/plugin-sdk test +pnpm --filter @droposs/plugin-cli test +``` From 1f04a51e78f246d434185aa2403a8222d6ac4e1f Mon Sep 17 00:00:00 2001 From: John Smith Date: Mon, 14 Sep 2026 01:39:34 -0400 Subject: [PATCH 176/243] docs(roadmap): record per-issue completion status and verification evidence --- docs/implementation/roadmap-status.md | 49 +++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 docs/implementation/roadmap-status.md diff --git a/docs/implementation/roadmap-status.md b/docs/implementation/roadmap-status.md new file mode 100644 index 000000000..389547ac3 --- /dev/null +++ b/docs/implementation/roadmap-status.md @@ -0,0 +1,49 @@ +# Roadmap Completion Status + +Status of the two plans delivered for Heretek-Games/drop. `✅` shipped with +tests and a pushed commit; `🟡` landable contract/reference implementation; +`⛔` blocked on external toolchains/credentials. + +## `` — Issue classification & core SPIs + +| Area | Status | Evidence | +| :----------------------------------------------------------------------------------------------- | :----- | :------------------------------------------------------------------------- | +| Fork-review fixes (#4): pipeline symlink guard, torrential `WaitMap`, RPC secret, plugin WS auth | ✅ | `develop` + torrential tests | +| Launch hardening (#55–#59), uninstaller tests, launch-handler override | ✅ | #56 model tests, #59 UI override | +| `MetadataProvider` / `PaymentGateway` SPI | ✅ | `plugins.test.ts`, boundary plan | +| `CloudSavePathResolver` SPI (server + desktop + SDK schema) | ✅ | commit `885bc3d1`, SDK `571d476` | +| `StoreScanner` SPI + store plugins | ✅ | SDK + 4 store repos | +| New plugin repositories (17) | ✅ | all created, built, tested, pushed | +| Upstream PR 1 (platform/stability) & PR 2 (generic SPI) | 🟡 | `docs/implementation/upstream-pr{1,2}-*.md`, branches `upstream/pr{1,2}-*` | + +## `` — Client/Server cloud saves + +| Item | Status | Evidence | +| :----------------------------------------------------------------- | :----- | :------------------------------------------------------ | +| Ludusavi CLI wrapper + `.tar.zst` pack/unpack + SHA-256 | ✅ | `cloud_saves/src/ludusavi.rs` (10 tests) | +| Lifecycle hooks with real title + Proton/UMU prefix | ✅ | `process_manager.rs` | +| Authenticated server push/pull + size/history limits | ✅ | `transport.rs`, `sync.rs`, SaveManager tests | +| Object-permission fix (owner read) | ✅ | `saves/manager.ts` | +| Hybrid conflict policy (rollback archive + modal) | ✅ | `sync::decide_pre_launch`, `CloudSaveConflictModal.vue` | +| `SaveSlotManager.vue` wired into game detail | ✅ | `pages/library/[id]/index.vue` | +| Managed `ludusavi` tool provisioning into `/tools/ludusavi/` | 🟡 | discovery implemented; automated download pending | + +## Tranches 4–7 + +| Tranche | Status | Notes | +| :--------------------------------- | :----- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| #17 Content delivery | 🟡 | `downpour push` CDN chunker + LAN peer contract ✅; mDNS/SSDP backend, S3 uploader, DXVK hook ✅/🟡 | +| #18 Handheld/Input/Overlay | 🟡 | `input` crate (deadzone/gyro/backend trait) + Big Picture page ✅; uinput/ViGEm backends ⛔ (need `/dev/uinput`/ViGEm), Vulkan/DXGI overlay ⛔ (Vulkan layer toolchain) | +| #8 Achievements | ✅ | Prisma models + migration, unlock API, `drop:achievement:unlock` bus, GSE `achievements.json` parser; desktop toasts 🟡 | +| #20/#21 Social/Federation/Commerce | 🟡 | verified reviews + Ed25519 receipts + community page ✅; federation presence ✅; payments webhooks & multi-store wiring 🟡 | + +## Verification (all green) + +```sh +pnpm --filter drop run test # 9/9 files +pnpm --filter drop run typecheck +cargo +nightly test -p cloud_saves -p database -p input +cd torrential && cargo +nightly test +pnpm -r run test # drop-plugin-sdk +# each of the 17 plugin repos: npm run build && npm test +``` From 775c2f0ceadc62f0564c3d879d1e5470418026be Mon Sep 17 00:00:00 2001 From: John Smith Date: Mon, 14 Sep 2026 01:43:21 -0400 Subject: [PATCH 177/243] refactor(metadata): consume plugin MetadataProvider SPI and externalize PCGamingWiki (#310) - Add PluginMetadataProvider, adapting the external MetadataProvider SPI to core's metadata contract, and register adapted providers after plugin init in 08.plugin-system.ts. - Map plugin ids to the MetadataSource enum; extend the enum with SteamGridDB, LaunchBox, ScreenScraper, and MobyGames. - Remove the in-core PCGamingWiki scraper; it now lives in drop-metadata-pcgamingwiki. --- .../migration.sql | 5 + server/prisma/models/content.prisma | 4 + .../metadata/__tests__/plugin-sources.test.ts | 33 ++ .../server/internal/metadata/pcgamingwiki.ts | 505 ------------------ .../internal/metadata/plugin-provider.ts | 99 ++++ .../internal/metadata/plugin-sources.ts | 20 + server/server/plugins/03.metadata-init.ts | 5 +- server/server/plugins/08.plugin-system.ts | 14 + 8 files changed, 178 insertions(+), 507 deletions(-) create mode 100644 server/prisma/migrations/20260914040000_add_metadata_sources/migration.sql create mode 100644 server/server/internal/metadata/__tests__/plugin-sources.test.ts delete mode 100644 server/server/internal/metadata/pcgamingwiki.ts create mode 100644 server/server/internal/metadata/plugin-provider.ts create mode 100644 server/server/internal/metadata/plugin-sources.ts diff --git a/server/prisma/migrations/20260914040000_add_metadata_sources/migration.sql b/server/prisma/migrations/20260914040000_add_metadata_sources/migration.sql new file mode 100644 index 000000000..61d5968dc --- /dev/null +++ b/server/prisma/migrations/20260914040000_add_metadata_sources/migration.sql @@ -0,0 +1,5 @@ +-- AddValue +ALTER TYPE "MetadataSource" ADD VALUE IF NOT EXISTS 'SteamGridDB'; +ALTER TYPE "MetadataSource" ADD VALUE IF NOT EXISTS 'LaunchBox'; +ALTER TYPE "MetadataSource" ADD VALUE IF NOT EXISTS 'ScreenScraper'; +ALTER TYPE "MetadataSource" ADD VALUE IF NOT EXISTS 'MobyGames'; diff --git a/server/prisma/models/content.prisma b/server/prisma/models/content.prisma index 18351ee88..6714c8705 100644 --- a/server/prisma/models/content.prisma +++ b/server/prisma/models/content.prisma @@ -6,6 +6,10 @@ enum MetadataSource { IGDB Metacritic OpenCritic + SteamGridDB + LaunchBox + ScreenScraper + MobyGames } enum GameType { diff --git a/server/server/internal/metadata/__tests__/plugin-sources.test.ts b/server/server/internal/metadata/__tests__/plugin-sources.test.ts new file mode 100644 index 000000000..716077de4 --- /dev/null +++ b/server/server/internal/metadata/__tests__/plugin-sources.test.ts @@ -0,0 +1,33 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { MetadataSource } from "~/prisma/client/enums"; +import { + PLUGIN_METADATA_SOURCES, + metadataSourceForPluginId, +} from "../plugin-sources"; + +test("known metadata plugins map to core MetadataSource values", () => { + assert.equal( + metadataSourceForPluginId("pcgamingwiki"), + MetadataSource.PCGamingWiki, + ); + assert.equal( + metadataSourceForPluginId("steamgriddb"), + MetadataSource.SteamGridDB, + ); + assert.equal( + metadataSourceForPluginId("mobygames"), + MetadataSource.MobyGames, + ); +}); + +test("unknown metadata plugins are not persistable against the core enum", () => { + assert.equal(metadataSourceForPluginId("not-a-provider"), undefined); +}); + +test("every mapped value is a real MetadataSource enum member", () => { + const valid = new Set(Object.values(MetadataSource)); + for (const source of Object.values(PLUGIN_METADATA_SOURCES)) { + assert.ok(valid.has(source), `unexpected MetadataSource: ${source}`); + } +}); diff --git a/server/server/internal/metadata/pcgamingwiki.ts b/server/server/internal/metadata/pcgamingwiki.ts deleted file mode 100644 index 85634d0aa..000000000 --- a/server/server/internal/metadata/pcgamingwiki.ts +++ /dev/null @@ -1,505 +0,0 @@ -import type { CompanyModel } from "~/prisma/client/models"; -import { MetadataSource } from "~/prisma/client/enums"; -import type { MetadataProvider } from "."; -import type { - GameMetadataSearchResult, - _FetchGameMetadataParams, - GameMetadata, - _FetchCompanyMetadataParams, - CompanyMetadata, - GameMetadataRating, -} from "./types"; -import * as jdenticon from "jdenticon"; -import { DateTime } from "luxon"; -import * as cheerio from "cheerio"; -import { type } from "arktype"; -import type { TaskRunContext } from "../tasks"; -import { logger } from "~/server/internal/logging"; -import type { NitroFetchOptions, NitroFetchRequest } from "nitropack"; - -interface PCGamingWikiParseRawPage { - parse: { - title: string; - pageid: number; - revid: number; - displaytitle: string; - // array of links - externallinks: string[]; - // array of wiki file names - images: string[]; - text: { - // rendered page contents - "*": string; - }; - }; -} - -interface PCGamingWikiPage { - PageID: string; - PageName: string; -} - -interface PCGamingWikiSearchStub extends PCGamingWikiPage { - "Cover URL": string | null; - Released: string | null; - Released__precision: string | null; -} - -type WikiStringList = string | string[] | null; - -interface PCGamingWikiGame extends PCGamingWikiSearchStub { - Developers: WikiStringList; - Publishers: WikiStringList; - - // TODO: save this somewhere, maybe a tag? - Series: string | null; - - // tags - Perspectives: WikiStringList; // ie: First-person - Genres: WikiStringList; // ie: Action, FPS - "Art styles": WikiStringList; // ie: Stylized - Themes: WikiStringList; // ie: Post-apocalyptic, Sci-fi, Space - Modes: WikiStringList; // ie: Singleplayer, Multiplayer - Pacing: WikiStringList; // ie: Real-time -} - -interface PCGamingWikiCompany extends PCGamingWikiPage { - Parent: string | null; - Founded: string | null; - Website: string | null; - Founded__precision: string | null; - Defunct__precision: string | null; -} - -interface PCGamingWikiCargoResult { - cargoquery: [ - { - title: T; - }, - ]; - error?: { - code?: string; - info?: string; - errorclass?: string; - "*"?: string; - }; -} - -type StringArrayKeys = { - [K in keyof T]: T[K] extends string | string[] | null ? K : never; -}[keyof T]; - -const ratingProviderReview = type({ - rating: "string.integer.parse", -}); - -// Api Docs: https://www.pcgamingwiki.com/wiki/PCGamingWiki:API -// Good tool for helping build cargo queries: https://www.pcgamingwiki.com/wiki/Special:CargoQuery -export class PCGamingWikiProvider implements MetadataProvider { - name() { - return "PCGamingWiki"; - } - source() { - return MetadataSource.PCGamingWiki; - } - - private async request( - query: URLSearchParams, - options?: NitroFetchOptions, - ) { - const finalURL = `https://www.pcgamingwiki.com/w/api.php?${query.toString()}`; - - const response = await $fetch(finalURL, options); - - return response; - } - - private async cargoQuery( - query: URLSearchParams, - options?: NitroFetchOptions, - ) { - const response = await this.request>( - query, - options, - ); - if (response.error !== undefined) - throw new Error(`Error in pcgamingwiki cargo query`); - return response; - } - - /** - * Gets the raw wiki page for parsing, - * requested values are to be considered unstable as compared to cargo queries - * @param pageID - * @returns - */ - private async getPageContent(pageID: string) { - const searchParams = new URLSearchParams({ - action: "parse", - format: "json", - pageid: pageID, - }); - const res = await this.request(searchParams); - const $ = cheerio.load(res.parse.text["*"]); - // get intro based on 'introduction' class - const introductionEle = $(".introduction").first(); - // remove citations from intro - introductionEle.find("sup").remove(); - - const infoBoxEle = $(".template-infobox").first(); - const receptionEle = infoBoxEle - .find(".template-infobox-header") - .filter((_, el) => $(el).text().trim() === "Reception"); - - const receptionResults: (GameMetadataRating | undefined)[] = []; - if (receptionEle.length > 0) { - // we have a match! - - const ratingElements = infoBoxEle.find(".template-infobox-type"); - - // TODO: cleanup this ratnest - const parseIdFromHref = (href: string): string | undefined => { - const url = new URL(href); - const opencriticRegex = /^\/game\/(\d+)\/.+$/; - switch (url.hostname.toLocaleLowerCase()) { - case "www.metacritic.com": { - // https://www.metacritic.com/game/elden-ring/critic-reviews/?platform=pc - return url.pathname - .replace("/game/", "") - .replace("/critic-reviews", "") - .replace(/\/$/, ""); - } - case "opencritic.com": { - // https://opencritic.com/game/12090/elden-ring - let id = "unknown"; - let matches; - if ((matches = opencriticRegex.exec(url.pathname)) !== null) { - matches.forEach((match, _groupIndex) => { - // logger.log(`Found match, group ${_groupIndex}: ${match}`); - id = match; - }); - } - - if (id === "unknown") { - return undefined; - } - return id; - } - case "www.igdb.com": { - // https://www.igdb.com/games/elden-ring - return url.pathname.replace("/games/", "").replace(/\/$/, ""); - } - default: { - logger.warn( - { hostname: url.hostname }, - "Pcgamingwiki, unknown host", - ); - return undefined; - } - } - }; - const getRating = ( - source: MetadataSource, - ): GameMetadataRating | undefined => { - const providerEle = ratingElements.filter( - (_, el) => - $(el).text().trim().toLocaleLowerCase() === - source.toLocaleLowerCase(), - ); - if (providerEle.length > 0) { - // get info associated with provider - const reviewEle = providerEle - .first() - .parent() - .find(".template-infobox-info") - .find("a") - .first(); - - const href = reviewEle.attr("href"); - if (!href) { - logger.info( - `pcgamingwiki: failed to properly get review href for ${source}`, - ); - return undefined; - } - const ratingObj = ratingProviderReview({ - rating: reviewEle.text().trim(), - }); - if (ratingObj instanceof type.errors) { - logger.info( - { summary: ratingObj.summary }, - "pcgamingwiki: failed to properly get review rating", - ); - return undefined; - } - - const id = parseIdFromHref(href); - if (!id) return undefined; - - return { - mReviewHref: href, - metadataId: id, - metadataSource: source, - mReviewCount: 0, - // make float within 0 to 1 - mReviewRating: ratingObj.rating / 100, - }; - } - - return undefined; - }; - receptionResults.push( - getRating(MetadataSource.Metacritic), - getRating(MetadataSource.IGDB), - getRating(MetadataSource.OpenCritic), - ); - } - - return { - shortIntro: introductionEle.find("p").first().text().trim(), - introduction: introductionEle.text().trim(), - reception: receptionResults, - }; - } - - async search(query: string) { - const searchParams = new URLSearchParams({ - action: "cargoquery", - tables: "Infobox_game", - fields: - "Infobox_game._pageID=PageID,Infobox_game._pageName=PageName,Infobox_game.Cover_URL,Infobox_game.Released", - where: `Infobox_game._pageName="${query}"`, - format: "json", - }); - - const response = - await this.cargoQuery(searchParams); - - const results: GameMetadataSearchResult[] = []; - for (const result of response.cargoquery) { - const game = result.title; - const pageContent = await this.getPageContent(game.PageID); - - results.push({ - id: game.PageID, - name: game.PageName, - icon: game["Cover URL"] ?? "", - description: pageContent.shortIntro, - year: - game.Released !== null && game.Released.length > 0 - ? // sometimes will provide multiple dates - this.parseTS(game.Released).year - : 0, - }); - } - - return results; - } - - /** - * Parses the specific format that the wiki returns when specifying an array - * @param input string or array - * @returns - */ - private parseWikiStringArray(input: string | string[]): string[] { - const cleanStr = (str: string): string => { - // remove any dumb prefixes we don't care about - return str.replace("Company:", "").trim(); - }; - - // input can provides the string as a list - // ie: "Company:Digerati Distribution,Company:Greylock Studio" - // or as an array, sometimes the array has empty values - - const results: string[] = []; - if (Array.isArray(input)) { - input.forEach((c) => { - const clean = cleanStr(c); - if (clean !== "") results.push(clean); - }); - } else { - const items = input.split(","); - items.forEach((item) => { - const clean = cleanStr(item); - if (clean !== "") results.push(clean); - }); - } - - return results; - } - - /** - * Parses the specific format that the wiki returns when specifying a iso timestamp - * @param isoStr - * @returns - */ - private parseTS(isoStr: string): DateTime { - return DateTime.fromISO(isoStr.split(";")[0]!); - } - - private parseWebsitesGetFirst(websiteStr?: string | null): string { - if (websiteStr === undefined || websiteStr === null) return ""; - - // string comes in format: "[https://www.gamesci.com.cn www.gamesci.com.cn]" - return websiteStr.replaceAll(/[[\]]/g, "").split(" ")[0] ?? ""; - } - - private compileTags(game: PCGamingWikiGame): string[] { - const results: string[] = []; - - const properties: StringArrayKeys[] = [ - "Art styles", - "Genres", - "Modes", - "Pacing", - "Perspectives", - "Themes", - ]; - - // loop through all above keys, get the tags they contain - properties.forEach((p) => { - if (game[p] === null) return; - - results.push(...this.parseWikiStringArray(game[p])); - }); - - return results; - } - - async fetchGame( - { id, name, company, createObject }: _FetchGameMetadataParams, - context?: TaskRunContext, - ): Promise { - context?.logger.info("Using PCGamingWiki provider"); - context?.progress(0); - - const searchParams = new URLSearchParams({ - action: "cargoquery", - tables: "Infobox_game", - fields: - "Infobox_game._pageID=PageID,Infobox_game._pageName=PageName,Infobox_game.Cover_URL,Infobox_game.Developers,Infobox_game.Released,Infobox_game.Genres,Infobox_game.Publishers,Infobox_game.Themes,Infobox_game.Series,Infobox_game.Modes,Infobox_game.Perspectives,Infobox_game.Art_styles,Infobox_game.Pacing", - where: `Infobox_game._pageID="${id}"`, - format: "json", - }); - - const [res, pageContent] = await Promise.all([ - this.cargoQuery(searchParams), - this.getPageContent(id), - ]); - if (res.cargoquery.length < 1) - throw new Error("Error in pcgamingwiki, no game"); - - const game = res.cargoquery[0].title; - - const publishers: CompanyModel[] = []; - if (game.Publishers !== null) { - context?.logger.info("Found publishers, importing..."); - const pubListClean = this.parseWikiStringArray(game.Publishers); - for (const pub of pubListClean) { - context?.logger.info(`Importing publisher "${pub}"...`); - - const res = await company(pub); - if (res === undefined) { - context?.logger.warn(`Failed to import publisher "${pub}"`); - continue; - } - context?.logger.info(`Imported publisher "${pub}"`); - // add to publishers - publishers.push(res); - } - } - - context?.progress(40); - - const developers: CompanyModel[] = []; - if (game.Developers !== null) { - context?.logger.info("Found developers, importing..."); - const devListClean = this.parseWikiStringArray(game.Developers); - for (const dev of devListClean) { - context?.logger.info(`Importing developer "${dev}"...`); - const res = await company(dev); - if (res === undefined) { - context?.logger.warn(`Failed to import developer "${dev}"`); - continue; - } - context?.logger.info(`Imported developer "${dev}"`); - developers.push(res); - } - } - - context?.progress(80); - - const icon = createObject( - game["Cover URL"] !== null - ? game["Cover URL"] - : jdenticon.toPng(name, 512), - ); - - const released = game.Released - ? DateTime.fromISO(game.Released.split(";")[0]!).toJSDate() - : new Date(); - - const metadata: GameMetadata = { - id: game.PageID, - name: game.PageName, - shortDescription: pageContent.shortIntro, - description: pageContent.introduction, - released, - - tags: this.compileTags(game), - - reviews: pageContent.reception.filter((v) => v !== undefined), - ageRatings: [], - publishers, - developers, - - icon: icon, - bannerId: icon, - coverId: icon, - images: [icon], - }; - - context?.logger.info("PCGamingWiki provider finished."); - context?.progress(100); - - return metadata; - } - - async fetchCompany({ - query, - createObject, - }: _FetchCompanyMetadataParams): Promise { - const searchParams = new URLSearchParams({ - action: "cargoquery", - tables: "Company", - fields: - "Company.Parent,Company.Founded,Company.Defunct,Company.Website,Company._pageName=PageName,Company._pageID=PageID", - where: `Company._pageName="Company:${query}"`, - format: "json", - }); - - const res = await this.cargoQuery(searchParams); - - // TODO: replace with company logo - const icon = createObject(jdenticon.toPng(query, 512)); - - const item = res.cargoquery[0]; - if (!item) return undefined; - - const company = item.title; - - const fixedCompanyName = - this.parseWikiStringArray(company.PageName)[0] ?? company.PageName; - - return { - id: company.PageID, - name: fixedCompanyName, - shortDescription: "", - description: "", - website: this.parseWebsitesGetFirst(company?.Website), - - logo: icon, - banner: icon, - }; - } -} diff --git a/server/server/internal/metadata/plugin-provider.ts b/server/server/internal/metadata/plugin-provider.ts new file mode 100644 index 000000000..9b09a741b --- /dev/null +++ b/server/server/internal/metadata/plugin-provider.ts @@ -0,0 +1,99 @@ +import * as jdenticon from "jdenticon"; +import type { MetadataSource } from "~/prisma/client/enums"; +import { MetadataProvider } from "."; +import type { + CompanyMetadata, + GameMetadata, + GameMetadataSearchResult, + _FetchCompanyMetadataParams, + _FetchGameMetadataParams, +} from "./types"; +import type { MetadataProvider as ExternalMetadataProvider } from "../plugins/types"; + +export { metadataSourceForPluginId } from "./plugin-sources"; + +/** + * Adapts an external `MetadataProvider` SPI implementation to the core + * `MetadataProvider` contract so plugin-provided sources participate in the + * admin search/import flow. Companies are not resolved (the SPI does not expose + * company entities); developers/publishers come back as tags instead. + */ +export class PluginMetadataProvider extends MetadataProvider { + constructor( + private readonly plugin: ExternalMetadataProvider, + private readonly mappedSource: MetadataSource, + ) { + super(); + } + + name(): string { + return this.plugin.name; + } + + source(): MetadataSource { + return this.mappedSource; + } + + async search(query: string): Promise { + const results = await this.plugin.search(query); + return results.map((result) => ({ + id: result.id, + name: result.title, + icon: result.coverUrl ?? result.iconUrl ?? "", + description: result.description ?? "", + year: result.releaseYear ?? 0, + })); + } + + async fetchGame({ + id, + name, + createObject, + }: _FetchGameMetadataParams): Promise { + const details = await this.plugin.getDetails(id); + if (!details) { + throw new Error( + `Plugin metadata provider '${this.plugin.id}' has no entry for '${id}'`, + ); + } + + const icon = createObject( + details.coverUrl ?? details.iconUrl ?? jdenticon.toPng(name, 512), + ); + + const tags = new Set([ + ...(details.genres ?? []), + ...(details.developers ?? []), + ...(details.publishers ?? []), + ]); + + return { + id: details.id, + name: details.title, + shortDescription: details.description ?? "", + description: details.description ?? "", + released: details.releaseYear + ? new Date(details.releaseYear, 0, 1) + : new Date(), + + tags: [...tags], + + reviews: [], + ageRatings: [], + + publishers: [], + developers: [], + + icon, + bannerId: icon, + coverId: icon, + images: details.screenshots?.length ? details.screenshots : [icon], + }; + } + + async fetchCompany( + _params: _FetchCompanyMetadataParams, + ): Promise { + return undefined; + } +} diff --git a/server/server/internal/metadata/plugin-sources.ts b/server/server/internal/metadata/plugin-sources.ts new file mode 100644 index 000000000..bd73c40d0 --- /dev/null +++ b/server/server/internal/metadata/plugin-sources.ts @@ -0,0 +1,20 @@ +import { MetadataSource } from "~/prisma/client/enums"; + +/** + * Plugin id (from the external `MetadataProvider` SPI) to core `MetadataSource` + * enum. Plugins whose id is absent cannot be persisted against the strict core + * enum, so the registrar skips them. + */ +export const PLUGIN_METADATA_SOURCES: Record = { + pcgamingwiki: MetadataSource.PCGamingWiki, + steamgriddb: MetadataSource.SteamGridDB, + launchbox: MetadataSource.LaunchBox, + screenscraper: MetadataSource.ScreenScraper, + mobygames: MetadataSource.MobyGames, +}; + +export function metadataSourceForPluginId( + pluginId: string, +): MetadataSource | undefined { + return PLUGIN_METADATA_SOURCES[pluginId]; +} diff --git a/server/server/plugins/03.metadata-init.ts b/server/server/plugins/03.metadata-init.ts index 04e87f3cc..f6532ad46 100644 --- a/server/server/plugins/03.metadata-init.ts +++ b/server/server/plugins/03.metadata-init.ts @@ -3,12 +3,13 @@ import type { MetadataProvider } from "../internal/metadata"; import metadataHandler from "../internal/metadata"; import { IGDBProvider } from "../internal/metadata/igdb"; import { ManualMetadataProvider } from "../internal/metadata/manual"; -import { PCGamingWikiProvider } from "../internal/metadata/pcgamingwiki"; import { SteamProvider } from "../internal/metadata/steam"; import { logger } from "~/server/internal/logging"; export default defineNitroPlugin(async (_nitro) => { - const metadataProviders = [SteamProvider, PCGamingWikiProvider, IGDBProvider]; + // PCGamingWiki and the other community sources are external plugins now; they + // are registered through the MetadataProvider SPI in 08.plugin-system.ts. + const metadataProviders = [SteamProvider, IGDBProvider]; const providers = new Map(); diff --git a/server/server/plugins/08.plugin-system.ts b/server/server/plugins/08.plugin-system.ts index 0e4777537..b2c85f816 100644 --- a/server/server/plugins/08.plugin-system.ts +++ b/server/server/plugins/08.plugin-system.ts @@ -1,8 +1,22 @@ import { initPlugins, pluginManager } from "../internal/plugins"; +import metadataHandler from "../internal/metadata"; +import { + PluginMetadataProvider, + metadataSourceForPluginId, +} from "../internal/metadata/plugin-provider"; export default defineNitroPlugin(async (nitro) => { await initPlugins(); + // Consume the external MetadataProvider SPI: register adapted plugin + // providers so they participate in the admin search/import flow now that the + // in-core scrapers (e.g. PCGamingWiki) live in plugins. + for (const provider of pluginManager.getMetadataProviders()) { + const source = metadataSourceForPluginId(provider.id); + if (!source) continue; + metadataHandler.addProvider(new PluginMetadataProvider(provider, source)); + } + nitro.hooks.hookOnce("close", async () => { const plugins = pluginManager.listPlugins(); for (const plugin of plugins) { From 2a76fdfe4f42a1351f48b4ad4c6fe5a034d03fac Mon Sep 17 00:00:00 2001 From: John Smith Date: Mon, 14 Sep 2026 01:44:51 -0400 Subject: [PATCH 178/243] feat(cloud-saves): consume CloudSaveProvider resolver SPI in the server save path - Add resolveCloudSavePatterns(), aggregating tagged patterns from every registered CloudSaveProvider resolver and skipping failing ones. - Expose POST /api/v1/client/cloudsave/patterns for the desktop client to resolve save locations with its local install context. --- .../api/v1/client/cloudsave/patterns.post.ts | 40 +++++++++++++ .../saves/__tests__/resolvers.test.ts | 56 +++++++++++++++++++ server/server/internal/saves/resolvers.ts | 35 ++++++++++++ 3 files changed, 131 insertions(+) create mode 100644 server/server/api/v1/client/cloudsave/patterns.post.ts create mode 100644 server/server/internal/saves/__tests__/resolvers.test.ts create mode 100644 server/server/internal/saves/resolvers.ts diff --git a/server/server/api/v1/client/cloudsave/patterns.post.ts b/server/server/api/v1/client/cloudsave/patterns.post.ts new file mode 100644 index 000000000..fc6d2d095 --- /dev/null +++ b/server/server/api/v1/client/cloudsave/patterns.post.ts @@ -0,0 +1,40 @@ +import { defineClientEventHandler } from "~/server/internal/clients/event-handler"; +import pluginManager from "~/server/internal/plugins"; +import { resolveCloudSavePatterns } from "~/server/internal/saves/resolvers"; +import type { GameInstallContext } from "~/server/internal/plugins/types"; + +interface PatternBody { + gameId?: string; + gameTitle?: string; + installDir?: string; + winePrefix?: string; + executableName?: string; +} + +/** + * Resolves save-file patterns for a game through every registered + * `CloudSaveProvider` SPI resolver (e.g. the Ludusavi plugin). The desktop + * client sends its local install context because prefixes live client-side. + */ +export default defineClientEventHandler(async (h3) => { + const body = ((await readBody(h3)) ?? {}) as PatternBody; + if (!body.gameId || !body.gameTitle) { + throw createError({ + statusCode: 400, + statusMessage: "gameId and gameTitle are required", + }); + } + + const context: GameInstallContext = { + gameId: body.gameId, + gameTitle: body.gameTitle, + installDir: body.installDir, + winePrefix: body.winePrefix, + executableName: body.executableName, + }; + + return await resolveCloudSavePatterns( + pluginManager.getCloudSaveResolvers(), + context, + ); +}); diff --git a/server/server/internal/saves/__tests__/resolvers.test.ts b/server/server/internal/saves/__tests__/resolvers.test.ts new file mode 100644 index 000000000..05bb86b5e --- /dev/null +++ b/server/server/internal/saves/__tests__/resolvers.test.ts @@ -0,0 +1,56 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import type { CloudSavePathResolver } from "../../plugins/types"; +import { resolveCloudSavePatterns } from "../resolvers"; + +const context = { gameId: "game-1", gameTitle: "Hollow Knight" }; + +test("aggregates patterns from every resolver with its id", async () => { + const resolvers: CloudSavePathResolver[] = [ + { + id: "ludusavi", + name: "Ludusavi", + resolveSavePaths: async () => [ + { pattern: "%APPDATA%/Hollow Knight", winePrefix: true }, + ], + }, + { + id: "gamebox", + name: "GameBox", + resolveSavePaths: async () => [{ pattern: "~/.config/hollow" }], + }, + ]; + + const patterns = await resolveCloudSavePatterns(resolvers, context); + assert.equal(patterns.length, 2); + assert.deepEqual( + patterns.map((p) => p.resolverId), + ["ludusavi", "gamebox"], + ); + assert.equal(patterns[0].winePrefix, true); +}); + +test("a failing resolver does not break the others", async () => { + const resolvers: CloudSavePathResolver[] = [ + { + id: "broken", + name: "Broken", + resolveSavePaths: async () => { + throw new Error("boom"); + }, + }, + { + id: "ok", + name: "Ok", + resolveSavePaths: async () => [{ pattern: "~/.local/share/x" }], + }, + ]; + + const patterns = await resolveCloudSavePatterns(resolvers, context); + assert.equal(patterns.length, 1); + assert.equal(patterns[0].resolverId, "ok"); +}); + +test("no resolvers yields no patterns", async () => { + assert.deepEqual(await resolveCloudSavePatterns([], context), []); +}); diff --git a/server/server/internal/saves/resolvers.ts b/server/server/internal/saves/resolvers.ts new file mode 100644 index 000000000..7ea7ec75e --- /dev/null +++ b/server/server/internal/saves/resolvers.ts @@ -0,0 +1,35 @@ +import type { + CloudSavePathResolver, + CloudSavePattern, + GameInstallContext, +} from "../plugins/types"; + +export interface ResolvedCloudSavePattern extends CloudSavePattern { + resolverId: string; +} + +/** + * Asks every registered `CloudSaveProvider` SPI resolver for save-path patterns + * for a game, tagging each result with the resolver that supplied it. A failing + * resolver is skipped rather than failing the whole lookup. + */ +export async function resolveCloudSavePatterns( + resolvers: CloudSavePathResolver[], + context: GameInstallContext, +): Promise { + const resolved = await Promise.all( + resolvers.map(async (resolver) => { + try { + const patterns = await resolver.resolveSavePaths(context); + return patterns.map((pattern) => ({ + ...pattern, + resolverId: resolver.id, + })); + } catch { + return []; + } + }), + ); + + return resolved.flat(); +} From f8d57cef1a1dc736e287746a0ada6f7587397772 Mon Sep 17 00:00:00 2001 From: John Smith Date: Mon, 14 Sep 2026 01:46:52 -0400 Subject: [PATCH 179/243] feat(cloud-saves): provision managed Ludusavi into /tools/ludusavi - Add cloud_saves::provision with platform asset resolution, GitHub release tag parsing, tar.gz/raw extraction, and executable install, unit-tested. - Add a provision_ludusavi Tauri command and run a best-effort provisioning task on startup, mirroring the managed 7z/innoextract tools. --- desktop/src-tauri/Cargo.lock | 2 + desktop/src-tauri/Cargo.toml | 1 + desktop/src-tauri/cloud_saves/Cargo.toml | 1 + desktop/src-tauri/cloud_saves/src/lib.rs | 1 + .../src-tauri/cloud_saves/src/provision.rs | 205 ++++++++++++++++++ desktop/src-tauri/src/cloud_saves.rs | 56 +++++ desktop/src-tauri/src/lib.rs | 10 + 7 files changed, 276 insertions(+) create mode 100644 desktop/src-tauri/cloud_saves/src/provision.rs diff --git a/desktop/src-tauri/Cargo.lock b/desktop/src-tauri/Cargo.lock index 739877468..2b241f979 100644 --- a/desktop/src-tauri/Cargo.lock +++ b/desktop/src-tauri/Cargo.lock @@ -745,6 +745,7 @@ dependencies = [ "chrono", "database", "dirs 6.0.0", + "flate2", "log", "regex", "rustix 1.1.4", @@ -1363,6 +1364,7 @@ dependencies = [ "cacache", "chrono", "client", + "cloud_saves", "database", "deranged 0.4.0", "dirs 6.0.0", diff --git a/desktop/src-tauri/Cargo.toml b/desktop/src-tauri/Cargo.toml index 711b58768..37270e675 100644 --- a/desktop/src-tauri/Cargo.toml +++ b/desktop/src-tauri/Cargo.toml @@ -38,6 +38,7 @@ bytes = "1.10.1" cacache = "13.1.0" chrono = "0.4.38" client = { path = "./client", version = "0.1.0" } # client +cloud_saves = { path = "./cloud_saves", version = "0.1.0" } # cloud saves database = { path = "./database" } # database deranged = "=0.4.0" dirs = "6.0.0" diff --git a/desktop/src-tauri/cloud_saves/Cargo.toml b/desktop/src-tauri/cloud_saves/Cargo.toml index 1bc0b1281..ae0864434 100644 --- a/desktop/src-tauri/cloud_saves/Cargo.toml +++ b/desktop/src-tauri/cloud_saves/Cargo.toml @@ -7,6 +7,7 @@ edition = "2024" chrono = "0.4.42" database = { version = "0.1.0", path = "../database" } dirs = "6.0.0" +flate2 = "1.0" log = "0.4.28" regex = "1.11.3" rustix = "1.1.2" diff --git a/desktop/src-tauri/cloud_saves/src/lib.rs b/desktop/src-tauri/cloud_saves/src/lib.rs index d96054100..ede31a369 100644 --- a/desktop/src-tauri/cloud_saves/src/lib.rs +++ b/desktop/src-tauri/cloud_saves/src/lib.rs @@ -6,6 +6,7 @@ pub mod metadata; pub mod normalise; pub mod path; pub mod placeholder; +pub mod provision; pub mod resolver; pub mod sync; pub mod transport; diff --git a/desktop/src-tauri/cloud_saves/src/provision.rs b/desktop/src-tauri/cloud_saves/src/provision.rs new file mode 100644 index 000000000..89036d12a --- /dev/null +++ b/desktop/src-tauri/cloud_saves/src/provision.rs @@ -0,0 +1,205 @@ +//! Managed Ludusavi provisioning. +//! +//! Mirrors how `7z`/`innoextract` are provisioned into `/tools//`: +//! the desktop downloads the release archive for the host platform and drops +//! the binary there, where [`crate::LudusaviClient::discover`] finds it. +//! +//! Network access is injected (`download`) so the install logic stays testable. + +use std::fs; +use std::io::Read; +use std::path::{Path, PathBuf}; + +use database::db::DATA_ROOT_DIR; + +use crate::error::BackupError; + +const RELEASES_BASE: &str = "https://github.com/mtkennerly/ludusavi/releases"; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct LudusaviAsset { + pub tag: String, + pub file_name: String, + pub url: String, +} + +/// Builds the release asset for a concrete release tag (e.g. `v0.27.0`). +pub fn asset_for(tag: &str, os: &str, arch: &str) -> Option { + if tag.is_empty() { + return None; + } + let arch_token = match arch { + "x86_64" | "amd64" => "x64", + "aarch64" | "arm64" => "arm64", + _ => return None, + }; + let extension = match os { + "linux" | "macos" => "tar.gz", + "windows" => "zip", + _ => return None, + }; + let file_name = format!("ludusavi-{tag}-{os}-{arch_token}.{extension}"); + Some(LudusaviAsset { + tag: tag.to_string(), + url: format!("{RELEASES_BASE}/download/{tag}/{file_name}"), + file_name, + }) +} + +#[derive(serde::Deserialize)] +struct GithubRelease { + tag_name: String, +} + +/// Extracts `tag_name` from a GitHub `releases/latest` response. +pub fn parse_latest_tag(json: &str) -> Result { + let release: GithubRelease = serde_json::from_str(json)?; + if release.tag_name.is_empty() { + return Err(BackupError::ParseError); + } + Ok(release.tag_name) +} + +/// `/tools/ludusavi`. +pub fn tools_dir() -> PathBuf { + DATA_ROOT_DIR.join("tools").join("ludusavi") +} + +fn binary_name() -> &'static str { + if cfg!(target_os = "windows") { + "ludusavi.exe" + } else { + "ludusavi" + } +} + +/// Writes raw binary bytes to the tools directory and marks them executable. +pub fn install_binary(dest_dir: &Path, bytes: &[u8]) -> Result { + fs::create_dir_all(dest_dir)?; + let dest = dest_dir.join(binary_name()); + fs::write(&dest, bytes)?; + make_executable(&dest)?; + Ok(dest) +} + +/// Extracts the platform binary from a downloaded archive into `dest_dir`. +/// +/// Supports `.tar.gz` (Linux/macOS) and `.zip` (Windows); anything else is +/// treated as a raw binary. +pub fn install_archive( + file_name: &str, + bytes: &[u8], + dest_dir: &Path, +) -> Result { + if file_name.ends_with(".tar.gz") { + let decompressor = flate2::read::GzDecoder::new(bytes); + let mut archive = tar::Archive::new(decompressor); + for entry in archive.entries()? { + let mut entry = entry?; + let path = entry.path()?.to_path_buf(); + let name = path.file_name().and_then(|n| n.to_str()).unwrap_or(""); + if name != binary_name() { + continue; + } + let mut contents = Vec::new(); + entry.read_to_end(&mut contents)?; + return install_binary(dest_dir, &contents); + } + return Err(BackupError::NotFound); + } + + install_binary(dest_dir, bytes) +} + +#[cfg(unix)] +fn make_executable(path: &Path) -> Result<(), BackupError> { + use std::os::unix::fs::PermissionsExt; + let mut permissions = fs::metadata(path)?.permissions(); + permissions.set_mode(0o755); + fs::set_permissions(path, permissions)?; + Ok(()) +} + +#[cfg(not(unix))] +fn make_executable(_path: &Path) -> Result<(), BackupError> { + Ok(()) +} + +/// Downloads and installs Ludusavi for a concrete release tag. +pub fn provision(tag: &str, os: &str, arch: &str, download: F) -> Result +where + F: FnOnce(&str) -> Result, BackupError>, +{ + let asset = asset_for(tag, os, arch).ok_or(BackupError::NotFound)?; + let bytes = download(&asset.url)?; + install_archive(&asset.file_name, &bytes, &tools_dir()) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::io::Write; + use tempfile::tempdir; + + fn tar_gz_with(binary: &str, contents: &[u8]) -> Vec { + let mut gz = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::default()); + { + let mut builder = tar::Builder::new(&mut gz); + let mut header = tar::Header::new_gnu(); + header.set_size(contents.len() as u64); + header.set_mode(0o755); + header.set_cksum(); + builder + .append_data(&mut header, binary, contents) + .expect("append"); + builder.finish().expect("finish tar"); + } + gz.write_all(&[]).ok(); + gz.finish().expect("finish gz") + } + + #[test] + fn builds_platform_asset_urls() { + let linux = asset_for("v0.27.0", "linux", "x86_64").expect("linux asset"); + assert_eq!(linux.file_name, "ludusavi-v0.27.0-linux-x64.tar.gz"); + assert!(linux + .url + .ends_with("/download/v0.27.0/ludusavi-v0.27.0-linux-x64.tar.gz")); + + let mac = asset_for("v0.27.0", "macos", "aarch64").expect("mac asset"); + assert_eq!(mac.file_name, "ludusavi-v0.27.0-macos-arm64.tar.gz"); + + let win = asset_for("v0.27.0", "windows", "x86_64").expect("win asset"); + assert_eq!(win.file_name, "ludusavi-v0.27.0-windows-x64.zip"); + + assert!(asset_for("v0.27.0", "linux", "sparc").is_none()); + assert!(asset_for("", "linux", "x86_64").is_none()); + } + + #[test] + fn parses_the_latest_release_tag() { + let json = r#"{ "tag_name": "v0.27.0", "name": "v0.27.0" }"#; + assert_eq!(parse_latest_tag(json).unwrap(), "v0.27.0"); + } + + #[test] + fn installs_the_binary_from_a_tar_gz() { + let dir = tempdir().unwrap(); + let archive = tar_gz_with(binary_name(), b"LUDUSAVI_BINARY"); + let path = install_archive("ludusavi-v0.27.0-linux-x64.tar.gz", &archive, dir.path()) + .expect("install"); + assert_eq!(fs::read(&path).unwrap(), b"LUDUSAVI_BINARY"); + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + assert!(fs::metadata(&path).unwrap().permissions().mode() & 0o111 != 0); + } + } + + #[test] + fn installs_a_raw_binary() { + let dir = tempdir().unwrap(); + let path = install_archive("ludusavi", b"RAW", dir.path()).expect("install"); + assert_eq!(fs::read(&path).unwrap(), b"RAW"); + } +} diff --git a/desktop/src-tauri/src/cloud_saves.rs b/desktop/src-tauri/src/cloud_saves.rs index ef3964160..260a0f930 100644 --- a/desktop/src-tauri/src/cloud_saves.rs +++ b/desktop/src-tauri/src/cloud_saves.rs @@ -99,3 +99,59 @@ pub async fn download_cloud_save_object(object_id: String) -> Result/tools/ludusavi/`, +/// downloading the release for this platform on first use. Returns its path. +#[tauri::command] +pub async fn provision_ludusavi() -> Result { + if let Some(client) = cloud_saves::LudusaviClient::discover() { + return Ok(client.executable_path.to_string_lossy().to_string()); + } + + let release_url = + "https://api.github.com/repos/mtkennerly/ludusavi/releases/latest"; + let release_response = DROP_CLIENT_ASYNC + .get(release_url) + .header("User-Agent", "Drop Desktop Client") + .send() + .await + .map_err(|e| e.to_string())?; + if !release_response.status().is_success() { + return Err(format!( + "failed to query Ludusavi releases: {}", + release_response.status() + )); + } + let release_body = release_response.text().await.map_err(|e| e.to_string())?; + let tag = cloud_saves::provision::parse_latest_tag(&release_body) + .map_err(|e| e.to_string())?; + + let asset = cloud_saves::provision::asset_for( + &tag, + std::env::consts::OS, + std::env::consts::ARCH, + ) + .ok_or_else(|| "unsupported platform for Ludusavi".to_string())?; + + let asset_response = DROP_CLIENT_ASYNC + .get(&asset.url) + .header("User-Agent", "Drop Desktop Client") + .send() + .await + .map_err(|e| e.to_string())?; + if !asset_response.status().is_success() { + return Err(format!( + "failed to download Ludusavi: {}", + asset_response.status() + )); + } + let bytes = asset_response.bytes().await.map_err(|e| e.to_string())?; + + let path = cloud_saves::provision::install_archive( + &asset.file_name, + &bytes, + &cloud_saves::provision::tools_dir(), + ) + .map_err(|e| e.to_string())?; + Ok(path.to_string_lossy().to_string()) +} diff --git a/desktop/src-tauri/src/lib.rs b/desktop/src-tauri/src/lib.rs index 85ee058af..cd95e5df8 100644 --- a/desktop/src-tauri/src/lib.rs +++ b/desktop/src-tauri/src/lib.rs @@ -288,6 +288,7 @@ pub fn run() { // Cloud saves fetch_cloud_save_slots, download_cloud_save_object, + provision_ludusavi, // Community fetch_game_reviews, // Processes @@ -330,6 +331,15 @@ pub fn run() { app_handle_lock.replace(global_app_handle); }; + // Provision the managed Ludusavi tool on first launch (best + // effort; cloud saves simply stay local when it is unavailable). + tauri::async_runtime::spawn(async move { + match provision_ludusavi().await { + Ok(path) => debug!("managed Ludusavi available at {path}"), + Err(err) => debug!("Ludusavi provisioning skipped: {err}"), + } + }); + { use tauri_plugin_deep_link::DeepLinkExt; let _ = app.deep_link().register_all(); From bbdc7a2b2d7d94a1d9cea4aac8512a6df235d7a8 Mon Sep 17 00:00:00 2001 From: John Smith Date: Mon, 14 Sep 2026 01:49:50 -0400 Subject: [PATCH 180/243] feat(delivery): add shader-cache discovery hook and desktop export commands (#17) - Add the shader_cache crate: DXVK/VKD3D *.dxvk-cache discovery across the install dir and Proton/UMU prefix, plus collision-safe collection, tested with tempfiles. - Expose list_shader_caches/export_shader_caches Tauri commands; the community index lives in the drop-gamebox plugin. --- desktop/src-tauri/Cargo.lock | 9 ++ desktop/src-tauri/Cargo.toml | 2 + desktop/src-tauri/shader_cache/Cargo.toml | 13 +++ desktop/src-tauri/shader_cache/src/lib.rs | 133 ++++++++++++++++++++++ desktop/src-tauri/src/lib.rs | 5 + desktop/src-tauri/src/shaders.rs | 39 +++++++ 6 files changed, 201 insertions(+) create mode 100644 desktop/src-tauri/shader_cache/Cargo.toml create mode 100644 desktop/src-tauri/shader_cache/src/lib.rs create mode 100644 desktop/src-tauri/src/shaders.rs diff --git a/desktop/src-tauri/Cargo.lock b/desktop/src-tauri/Cargo.lock index 2b241f979..afeff7fc0 100644 --- a/desktop/src-tauri/Cargo.lock +++ b/desktop/src-tauri/Cargo.lock @@ -1403,6 +1403,7 @@ dependencies = [ "serde_with", "sha1", "sha2", + "shader_cache", "shared_child", "sysinfo 0.36.1", "tar", @@ -5270,6 +5271,14 @@ dependencies = [ "digest", ] +[[package]] +name = "shader_cache" +version = "0.1.0" +dependencies = [ + "tempfile", + "walkdir", +] + [[package]] name = "shared_child" version = "1.1.1" diff --git a/desktop/src-tauri/Cargo.toml b/desktop/src-tauri/Cargo.toml index 37270e675..4692ea76a 100644 --- a/desktop/src-tauri/Cargo.toml +++ b/desktop/src-tauri/Cargo.toml @@ -16,6 +16,7 @@ members = [ "input", "process", "remote", + "shader_cache", "utils", ] @@ -66,6 +67,7 @@ rand = "0.10.2" rayon = "1.10.0" regex = "1.11.1" remote = { path = "./remote", version = "0.1.0" } # remote +shader_cache = { path = "./shader_cache", version = "0.1.0" } # shader cache hooks reqwest = { version = "0.12.28", default-features = false, features = [ "blocking", "http2", diff --git a/desktop/src-tauri/shader_cache/Cargo.toml b/desktop/src-tauri/shader_cache/Cargo.toml new file mode 100644 index 000000000..53f3c173b --- /dev/null +++ b/desktop/src-tauri/shader_cache/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "shader_cache" +version = "0.1.0" +edition = "2024" + +[dependencies] +walkdir = "2.5" + +[dev-dependencies] +tempfile = "3" + +[lib] +path = "src/lib.rs" diff --git a/desktop/src-tauri/shader_cache/src/lib.rs b/desktop/src-tauri/shader_cache/src/lib.rs new file mode 100644 index 000000000..4f51f5db1 --- /dev/null +++ b/desktop/src-tauri/shader_cache/src/lib.rs @@ -0,0 +1,133 @@ +//! DXVK/VKD3D shader-cache discovery (Phase 1, #17). +//! +//! DXVK writes `*.dxvk-cache` state caches next to the executable or inside the +//! Proton/UMU prefix. The core client locates and collects them so they can be +//! uploaded to a community index (see the `drop-gamebox` plugin) or restored on +//! another machine. + +use std::fs; +use std::io; +use std::path::{Path, PathBuf}; + +use walkdir::WalkDir; + +pub const SHADER_CACHE_EXTENSION: &str = "dxvk-cache"; + +/// Whether a path is a DXVK/VKD3D state cache file. +#[must_use] +pub fn is_shader_cache_file(path: &Path) -> bool { + path.extension() + .and_then(|extension| extension.to_str()) + .is_some_and(|extension| extension.eq_ignore_ascii_case(SHADER_CACHE_EXTENSION)) +} + +/// Finds state cache files under the install directory and any extra roots +/// (typically the Proton/UMU prefix `drive_c` tree). Results are sorted and +/// deduplicated. +#[must_use] +pub fn find_shader_caches(install_dir: &Path, extra_roots: &[PathBuf]) -> Vec { + let mut roots = vec![install_dir.to_path_buf()]; + roots.extend(extra_roots.iter().cloned()); + + let mut found = Vec::new(); + for root in roots { + if !root.is_dir() { + continue; + } + for entry in WalkDir::new(&root).into_iter().filter_map(Result::ok) { + if entry.file_type().is_file() && is_shader_cache_file(entry.path()) { + found.push(entry.path().to_path_buf()); + } + } + } + + found.sort(); + found.dedup(); + found +} + +/// Copies every discovered cache file into `dest`, returning the copied paths. +/// Filenames are flattened with a numeric suffix on collision. +pub fn collect_shader_caches( + install_dir: &Path, + extra_roots: &[PathBuf], + dest: &Path, +) -> io::Result> { + fs::create_dir_all(dest)?; + + let mut copied = Vec::new(); + let mut seen: std::collections::HashSet = std::collections::HashSet::new(); + + for source in find_shader_caches(install_dir, extra_roots) { + let base = source + .file_name() + .and_then(|name| name.to_str()) + .unwrap_or("shader.dxvk-cache"); + let mut name = base.to_string(); + let mut counter = 1; + while !seen.insert(name.clone()) { + name = format!("{counter}-{base}"); + counter += 1; + } + let target = dest.join(&name); + fs::copy(&source, &target)?; + copied.push(target); + } + + Ok(copied) +} + +#[cfg(test)] +mod tests { + use super::*; + use tempfile::tempdir; + + #[test] + fn recognizes_only_dxvk_cache_files() { + assert!(is_shader_cache_file(Path::new("elden-ring.dxvk-cache"))); + assert!(is_shader_cache_file(Path::new("GAME.DXVK-CACHE"))); + assert!(!is_shader_cache_file(Path::new("game.exe"))); + assert!(!is_shader_cache_file(Path::new("no-extension"))); + } + + #[test] + fn finds_caches_across_install_and_prefix_roots() { + let install = tempdir().unwrap(); + let prefix = tempdir().unwrap(); + fs::create_dir_all(install.path().join("bin")).unwrap(); + fs::create_dir_all(prefix.path().join("drive_c/temp")).unwrap(); + + fs::write(install.path().join("bin/game.dxvk-cache"), b"cache-a").unwrap(); + fs::write( + prefix.path().join("drive_c/temp/game.dxvk-cache"), + b"cache-b", + ) + .unwrap(); + fs::write(install.path().join("bin/game.exe"), b"exe").unwrap(); + + let found = find_shader_caches(install.path(), &[prefix.path().to_path_buf()]); + assert_eq!(found.len(), 2); + } + + #[test] + fn collects_caches_with_collision_safe_names() { + let install = tempdir().unwrap(); + let prefix = tempdir().unwrap(); + let dest = tempdir().unwrap(); + fs::create_dir_all(install.path().join("a")).unwrap(); + fs::create_dir_all(prefix.path().join("b")).unwrap(); + fs::write(install.path().join("a/game.dxvk-cache"), b"one").unwrap(); + fs::write(prefix.path().join("b/game.dxvk-cache"), b"two").unwrap(); + + let copied = + collect_shader_caches(install.path(), &[prefix.path().to_path_buf()], dest.path()) + .unwrap(); + assert_eq!(copied.len(), 2); + let names: Vec = copied + .iter() + .map(|p| p.file_name().unwrap().to_string_lossy().to_string()) + .collect(); + assert!(names.contains(&"game.dxvk-cache".to_string())); + assert!(names.contains(&"1-game.dxvk-cache".to_string())); + } +} diff --git a/desktop/src-tauri/src/lib.rs b/desktop/src-tauri/src/lib.rs index cd95e5df8..c512f8e5b 100644 --- a/desktop/src-tauri/src/lib.rs +++ b/desktop/src-tauri/src/lib.rs @@ -70,6 +70,7 @@ mod process; mod remote; mod scheduler; mod settings; +mod shaders; mod updates; use client::*; @@ -82,6 +83,7 @@ use plugins::*; use process::*; use remote::*; use settings::*; +use shaders::*; use crate::scheduler::scheduler_task; @@ -291,6 +293,9 @@ pub fn run() { provision_ludusavi, // Community fetch_game_reviews, + // Shader caches + list_shader_caches, + export_shader_caches, // Processes launch_game, kill_game, diff --git a/desktop/src-tauri/src/shaders.rs b/desktop/src-tauri/src/shaders.rs new file mode 100644 index 000000000..561eecd34 --- /dev/null +++ b/desktop/src-tauri/src/shaders.rs @@ -0,0 +1,39 @@ +use std::path::{Path, PathBuf}; + +fn extra_roots(prefix_dir: Option) -> Vec { + prefix_dir + .filter(|path| !path.is_empty()) + .map(PathBuf::from) + .into_iter() + .collect() +} + +/// Lists DXVK/VKD3D state cache files for a game (install dir + optional prefix). +#[tauri::command] +pub fn list_shader_caches(install_dir: String, prefix_dir: Option) -> Vec { + shader_cache::find_shader_caches(Path::new(&install_dir), &extra_roots(prefix_dir)) + .into_iter() + .map(|path| path.to_string_lossy().to_string()) + .collect() +} + +/// Copies a game's shader caches into `dest` for upload to a community index. +#[tauri::command] +pub fn export_shader_caches( + install_dir: String, + prefix_dir: Option, + dest: String, +) -> Result, String> { + shader_cache::collect_shader_caches( + Path::new(&install_dir), + &extra_roots(prefix_dir), + Path::new(&dest), + ) + .map(|paths| { + paths + .into_iter() + .map(|path| path.to_string_lossy().to_string()) + .collect() + }) + .map_err(|err| err.to_string()) +} From c6f511f87b488b18a0c45afd3001b544c48441d7 Mon Sep 17 00:00:00 2001 From: John Smith Date: Mon, 14 Sep 2026 01:51:02 -0400 Subject: [PATCH 181/243] feat(lan): add SSDP peer discovery backend with loopback test (#17) - Parse M-SEARCH/response datagrams, build search queries, and map LOCATION URLs to LAN peers; the socket search loop is covered by a loopback UDP responder test. --- torrential/src/lan/mod.rs | 2 + torrential/src/lan/ssdp.rs | 251 +++++++++++++++++++++++++++++++++++++ 2 files changed, 253 insertions(+) create mode 100644 torrential/src/lan/ssdp.rs diff --git a/torrential/src/lan/mod.rs b/torrential/src/lan/mod.rs index 2ee145a6b..51e7acd14 100644 --- a/torrential/src/lan/mod.rs +++ b/torrential/src/lan/mod.rs @@ -6,6 +6,8 @@ //! QUIC/HTTP transport are separate integrations; this module fixes the //! selection + expiry behaviour both must agree on. +pub mod ssdp; + use std::net::SocketAddr; use std::time::{Duration, Instant}; diff --git a/torrential/src/lan/ssdp.rs b/torrential/src/lan/ssdp.rs new file mode 100644 index 000000000..d0890e4c8 --- /dev/null +++ b/torrential/src/lan/ssdp.rs @@ -0,0 +1,251 @@ +//! SSDP discovery backend for LAN depot peers. +//! +//! Drop depot servers announce themselves over SSDP; a downloading client +//! sends an `M-SEARCH` and collects `HTTP/1.1 200 OK` responses. The parsing +//! and message construction are pure and unit-tested; the socket loop is +//! exercised against a loopback responder in tests. + +use std::io; +use std::net::{SocketAddr, UdpSocket}; +use std::time::{Duration, Instant}; + +use super::LanPeer; + +/// A parsed SSDP response. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SsdpAnnouncement { + pub location: String, + pub usn: String, + pub server: Option, +} + +/// Builds an `M-SEARCH` datagram for a search target (e.g. `urn:drop:depot`). +#[must_use] +pub fn build_m_search(search_target: &str, mx: u8) -> String { + format!( + "M-SEARCH * HTTP/1.1\r\n\ + HOST: 239.255.255.250:1900\r\n\ + MAN: \"ssdp:discover\"\r\n\ + MX: {mx}\r\n\ + ST: {search_target}\r\n\ + \r\n" + ) +} + +/// Parses an SSDP response datagram, returning `None` for non-response lines or +/// announcements without a `LOCATION`. +#[must_use] +pub fn parse_ssdp_response(message: &str) -> Option { + let mut is_response = false; + let mut location = None; + let mut usn = None; + let mut server = None; + + for (index, raw_line) in message.lines().enumerate() { + let line = raw_line.trim(); + if index == 0 { + is_response = line.to_ascii_uppercase().starts_with("HTTP/1.1 200"); + continue; + } + let Some((name, value)) = line.split_once(':') else { + continue; + }; + let value = value.trim().to_string(); + match name.trim().to_ascii_uppercase().as_str() { + "LOCATION" => location = Some(value), + "USN" => usn = Some(value), + "SERVER" => server = Some(value), + _ => {} + } + } + + if !is_response { + return None; + } + + Some(SsdpAnnouncement { + location: location?, + usn: usn.unwrap_or_default(), + server, + }) +} + +/// Extracts a `host:port` peer address from an SSDP `LOCATION` URL. +#[must_use] +pub fn peer_address_from_location(location: &str) -> Option { + let without_scheme = location + .strip_prefix("http://") + .or_else(|| location.strip_prefix("https://")) + .unwrap_or(location); + let authority = without_scheme.split('/').next()?; + let authority = authority.rsplit('@').next()?; + authority.parse().ok() +} + +/// Converts an announcement into a [`LanPeer`], using the location host/port. +#[must_use] +pub fn announcement_to_peer(announcement: &SsdpAnnouncement) -> Option { + Some(LanPeer { + peer_id: announcement.usn.clone(), + address: peer_address_from_location(&announcement.location)?, + rtt_ms: 0, + throughput_kibps: 0, + }) +} + +/// Blocking SSDP searcher. `bind` is normally `0.0.0.0:0`; tests point `target` +/// at a loopback responder. +pub struct SsdpDiscovery { + bind: SocketAddr, + target: SocketAddr, + search_target: String, + timeout: Duration, +} + +impl SsdpDiscovery { + #[must_use] + pub fn new( + bind: SocketAddr, + target: SocketAddr, + search_target: &str, + timeout: Duration, + ) -> Self { + Self { + bind, + target, + search_target: search_target.to_string(), + timeout, + } + } + + /// Sends an `M-SEARCH` and collects responses until the timeout elapses. + /// + /// # Errors + /// Returns an I/O error if the socket cannot be bound or read. + pub fn search(&self) -> io::Result> { + let socket = UdpSocket::bind(self.bind)?; + socket.set_read_timeout(Some(self.timeout))?; + socket.send_to( + build_m_search(&self.search_target, 2).as_bytes(), + self.target, + )?; + + let mut announcements = Vec::new(); + let deadline = Instant::now() + self.timeout; + let mut buffer = [0u8; 2048]; + while Instant::now() < deadline { + match socket.recv_from(&mut buffer) { + Ok((size, _)) => { + let text = String::from_utf8_lossy(&buffer[..size]); + if let Some(announcement) = parse_ssdp_response(&text) { + announcements.push(announcement); + } + } + Err(error) + if error.kind() == io::ErrorKind::WouldBlock + || error.kind() == io::ErrorKind::TimedOut => + { + break; + } + Err(error) => return Err(error), + } + } + Ok(announcements) + } + + /// Discovered peers, ready for the LAN registry. + /// + /// # Errors + /// Returns an I/O error if the underlying SSDP search fails. + pub fn discover_peers(&self) -> io::Result> { + Ok(self + .search()? + .iter() + .filter_map(announcement_to_peer) + .collect()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + const RESPONSE: &str = "HTTP/1.1 200 OK\r\n\ + CACHE-CONTROL: max-age=1800\r\n\ + LOCATION: http://192.168.1.50:8080/depot\r\n\ + SERVER: Drop/0.4 UPnP/1.0\r\n\ + ST: urn:drop:depot\r\n\ + USN: uuid:drop-abc::urn:drop:depot\r\n\r\n"; + + #[test] + fn parses_an_ssdp_response() { + let Some(announcement) = parse_ssdp_response(RESPONSE) else { + panic!("response should parse"); + }; + assert_eq!(announcement.location, "http://192.168.1.50:8080/depot"); + assert_eq!(announcement.usn, "uuid:drop-abc::urn:drop:depot"); + assert_eq!(announcement.server.as_deref(), Some("Drop/0.4 UPnP/1.0")); + } + + #[test] + fn rejects_non_responses_and_missing_location() { + assert!(parse_ssdp_response("NOTIFY * HTTP/1.1\r\nLOCATION: x\r\n").is_none()); + assert!(parse_ssdp_response("HTTP/1.1 200 OK\r\nUSN: x\r\n").is_none()); + } + + #[test] + fn builds_an_m_search_datagram() { + let datagram = build_m_search("urn:drop:depot", 2); + assert!(datagram.starts_with("M-SEARCH * HTTP/1.1")); + assert!(datagram.contains("ST: urn:drop:depot")); + assert!(datagram.contains("MAN: \"ssdp:discover\"")); + } + + #[test] + fn extracts_peer_addresses_from_locations() { + assert_eq!( + peer_address_from_location("http://192.168.1.50:8080/depot"), + Some(SocketAddr::from(([192, 168, 1, 50], 8080))) + ); + assert_eq!( + peer_address_from_location("http://user@10.0.0.2:9000/x"), + Some(SocketAddr::from(([10, 0, 0, 2], 9000))) + ); + assert!(peer_address_from_location("not a url").is_none()); + } + + #[test] + fn discovers_a_loopback_responder() { + let Ok(responder) = UdpSocket::bind("127.0.0.1:0") else { + panic!("bind responder"); + }; + let Ok(responder_addr) = responder.local_addr() else { + panic!("responder address"); + }; + + let handle = std::thread::spawn(move || { + let mut buffer = [0u8; 1024]; + if let Ok((_, from)) = responder.recv_from(&mut buffer) { + let _ = responder.send_to(RESPONSE.as_bytes(), from); + } + }); + + let discovery = SsdpDiscovery::new( + SocketAddr::from(([127, 0, 0, 1], 0)), + responder_addr, + "urn:drop:depot", + Duration::from_millis(500), + ); + + let Ok(announcements) = discovery.search() else { + panic!("search should succeed"); + }; + handle.join().ok(); + + assert_eq!(announcements.len(), 1); + assert_eq!( + announcement_to_peer(&announcements[0]).map(|peer| peer.address), + Some(SocketAddr::from(([192, 168, 1, 50], 8080))) + ); + } +} From 8ef82917e1bd6f81d9b2b1c6d1c7a66833303715 Mon Sep 17 00:00:00 2001 From: John Smith Date: Mon, 14 Sep 2026 01:51:32 -0400 Subject: [PATCH 182/243] feat(lan): add client-to-client chunk fetch contract (#17) --- torrential/src/lan/mod.rs | 79 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/torrential/src/lan/mod.rs b/torrential/src/lan/mod.rs index 51e7acd14..79d1d492a 100644 --- a/torrential/src/lan/mod.rs +++ b/torrential/src/lan/mod.rs @@ -94,10 +94,89 @@ pub fn peer_score(peer: &LanPeer) -> u64 { latency * 1_000_000 + throughput } +/// Fetches a chunk from a LAN peer (HTTP/QUIC transport is injected). +pub trait ChunkFetcher: Send + Sync { + /// Returns the raw chunk bytes, or an error string on failure. + /// + /// # Errors + /// Transport-specific failure described as a string. + fn fetch(&self, peer: &LanPeer, content_id: &str, chunk_index: u64) -> Result, String>; +} + +/// Selects the best live peer and fetches a chunk through `fetcher`. Returns +/// `None` when no live peer is available. +pub fn fetch_from_best_peer( + registry: &LanPeerRegistry, + now: Instant, + fetcher: &F, + content_id: &str, + chunk_index: u64, +) -> Option, String>> { + let peer = registry.best(now)?; + Some(fetcher.fetch(&peer, content_id, chunk_index)) +} + #[cfg(test)] mod tests { use super::*; + #[test] + fn fetches_a_chunk_from_the_best_peer() { + struct RecordingFetcher { + seen: std::sync::Mutex>, + } + impl ChunkFetcher for RecordingFetcher { + fn fetch( + &self, + peer: &LanPeer, + content_id: &str, + chunk_index: u64, + ) -> Result, String> { + if let Ok(mut seen) = self.seen.lock() { + seen.push(peer.peer_id.clone()); + } + Ok(format!("{content_id}:{chunk_index}").into_bytes()) + } + } + + let mut registry = LanPeerRegistry::new(Duration::from_secs(30)); + let now = Instant::now(); + registry.observe(peer("slow", 50, 100), now); + registry.observe(peer("fast", 2, 100), now); + + let fetcher = RecordingFetcher { + seen: std::sync::Mutex::new(Vec::new()), + }; + let result = fetch_from_best_peer(®istry, now, &fetcher, "depot", 3); + let Some(Ok(bytes)) = result else { + panic!("expected a successful fetch"); + }; + assert_eq!(bytes, b"depot:3"); + assert_eq!( + fetcher.seen.lock().map(|seen| seen.clone()).ok(), + Some(vec!["fast".to_string()]) + ); + } + + #[test] + fn no_live_peer_yields_none() { + struct NeverFetcher; + impl ChunkFetcher for NeverFetcher { + fn fetch( + &self, + _peer: &LanPeer, + _content_id: &str, + _chunk_index: u64, + ) -> Result, String> { + Err("should not be called".to_string()) + } + } + let registry = LanPeerRegistry::new(Duration::from_secs(1)); + assert!( + fetch_from_best_peer(®istry, Instant::now(), &NeverFetcher, "depot", 0).is_none() + ); + } + fn peer(id: &str, rtt: u32, kibps: u32) -> LanPeer { LanPeer { peer_id: id.to_string(), From 49f2f612967f98e96684211409693396d06cd2b9 Mon Sep 17 00:00:00 2001 From: John Smith Date: Mon, 14 Sep 2026 01:53:01 -0400 Subject: [PATCH 183/243] feat(commerce): dispatch payment webhooks and wire store-scanner imports (#21) - Add a payment webhook dispatcher + public /api/v1/commerce/webhook/:gateway route that resolves the plugin-registered PaymentGateway and verifies its signature inside handleWebhook. - Add collectStoreGames() aggregating every StoreScanner SPI result with per-store failure isolation, plus a useStoreImport() composable. --- desktop/main/composables/useStoreImport.ts | 29 +++++++++++++ .../plugins/__tests__/storeImport.test.ts | 41 +++++++++++++++++++ desktop/main/internal/plugins/storeImport.ts | 32 +++++++++++++++ .../api/v1/commerce/webhook/[gateway].post.ts | 40 ++++++++++++++++++ .../commerce/__tests__/webhooks.test.ts | 39 ++++++++++++++++++ server/server/internal/commerce/webhooks.ts | 28 +++++++++++++ 6 files changed, 209 insertions(+) create mode 100644 desktop/main/composables/useStoreImport.ts create mode 100644 desktop/main/internal/plugins/__tests__/storeImport.test.ts create mode 100644 desktop/main/internal/plugins/storeImport.ts create mode 100644 server/server/api/v1/commerce/webhook/[gateway].post.ts create mode 100644 server/server/internal/commerce/__tests__/webhooks.test.ts create mode 100644 server/server/internal/commerce/webhooks.ts diff --git a/desktop/main/composables/useStoreImport.ts b/desktop/main/composables/useStoreImport.ts new file mode 100644 index 000000000..bef58b4ff --- /dev/null +++ b/desktop/main/composables/useStoreImport.ts @@ -0,0 +1,29 @@ +import { ref } from "vue"; +import { clientPluginManager } from "~/internal/plugins/ClientPluginManager"; +import { + collectStoreGames, + type StoreImportResult, +} from "~/internal/plugins/storeImport"; + +/** + * Runs every plugin-provided `StoreScanner` (Steam, GOG, Epic, itch.io) and + * exposes the aggregated result for the library import flow. + */ +export function useStoreImport() { + const scanning = ref(false); + const result = ref({ games: [], failures: [] }); + + async function scanAll(): Promise { + scanning.value = true; + try { + result.value = await collectStoreGames( + clientPluginManager.getStoreScanners(), + ); + } finally { + scanning.value = false; + } + return result.value; + } + + return { scanning, result, scanAll }; +} diff --git a/desktop/main/internal/plugins/__tests__/storeImport.test.ts b/desktop/main/internal/plugins/__tests__/storeImport.test.ts new file mode 100644 index 000000000..2b03ecddf --- /dev/null +++ b/desktop/main/internal/plugins/__tests__/storeImport.test.ts @@ -0,0 +1,41 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import type { StoreScanner } from "../types"; +import { collectStoreGames } from "../storeImport"; + +const steam: StoreScanner = { + id: "steam", + name: "Steam", + store: "steam", + scan: async () => [ + { + externalId: "570", + store: "steam", + title: "Team Fortress 2", + installPath: "/games/tf2", + }, + ], +}; + +const broken: StoreScanner = { + id: "epic", + name: "Epic", + store: "epic", + scan: async () => { + throw new Error("launcher not found"); + }, +}; + +test("collectStoreGames aggregates scanners and records failures", async () => { + const result = await collectStoreGames([steam, broken]); + assert.equal(result.games.length, 1); + assert.equal(result.games[0].title, "Team Fortress 2"); + assert.deepEqual(result.failures, [ + { store: "epic", error: "launcher not found" }, + ]); +}); + +test("collectStoreGames with no scanners returns empty", async () => { + const result = await collectStoreGames([]); + assert.deepEqual(result, { games: [], failures: [] }); +}); diff --git a/desktop/main/internal/plugins/storeImport.ts b/desktop/main/internal/plugins/storeImport.ts new file mode 100644 index 000000000..38a5cf510 --- /dev/null +++ b/desktop/main/internal/plugins/storeImport.ts @@ -0,0 +1,32 @@ +import type { ScannedGame, StoreScanner } from "./types"; + +export interface StoreImportResult { + games: ScannedGame[]; + failures: Array<{ store: string; error: string }>; +} + +/** + * Runs every plugin-registered `StoreScanner` SPI and aggregates the results. + * A scanner that throws is recorded as a failure and does not abort the scan, + * so one broken store integration cannot block a library import. + */ +export async function collectStoreGames( + scanners: StoreScanner[], +): Promise { + const games: ScannedGame[] = []; + const failures: Array<{ store: string; error: string }> = []; + + for (const scanner of scanners) { + try { + const discovered = await scanner.scan(); + games.push(...discovered); + } catch (error) { + failures.push({ + store: scanner.store, + error: error instanceof Error ? error.message : String(error), + }); + } + } + + return { games, failures }; +} diff --git a/server/server/api/v1/commerce/webhook/[gateway].post.ts b/server/server/api/v1/commerce/webhook/[gateway].post.ts new file mode 100644 index 000000000..b595e63de --- /dev/null +++ b/server/server/api/v1/commerce/webhook/[gateway].post.ts @@ -0,0 +1,40 @@ +import pluginManager from "~/server/internal/plugins"; +import { dispatchPaymentWebhook } from "~/server/internal/commerce/webhooks"; + +/** + * Provider-facing payment webhook endpoint. Public by design: each gateway + * verifies its own signature inside `handleWebhook`. + */ +export default defineEventHandler(async (h3) => { + const gatewayId = getRouterParam(h3, "gateway"); + if (!gatewayId) { + throw createError({ + statusCode: 400, + statusMessage: "No gateway in route", + }); + } + + const raw = (await readRawBody(h3, "utf8")) ?? ""; + let payload: unknown = raw; + try { + payload = JSON.parse(raw); + } catch { + // Leave `payload` as the raw body for gateways that verify raw bytes. + } + + const headers: Record = {}; + for (const [key, value] of Object.entries(h3.node.req.headers)) { + if (typeof value === "string") { + headers[key] = value; + } else if (Array.isArray(value)) { + headers[key] = value.join(", "); + } + } + + return await dispatchPaymentWebhook( + pluginManager, + gatewayId, + payload, + headers, + ); +}); diff --git a/server/server/internal/commerce/__tests__/webhooks.test.ts b/server/server/internal/commerce/__tests__/webhooks.test.ts new file mode 100644 index 000000000..d985103b8 --- /dev/null +++ b/server/server/internal/commerce/__tests__/webhooks.test.ts @@ -0,0 +1,39 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { dispatchPaymentWebhook } from "../webhooks"; +import type { PaymentGateway } from "../../plugins/types"; + +const gateway: PaymentGateway = { + id: "stripe", + name: "Stripe", + createPaymentIntent: async () => ({ intentId: "pi", status: "pending" }), + handleWebhook: async (payload) => ({ + orderId: "order-1", + status: "succeeded", + transactionId: "txn-1", + payload: payload as Record, + }), +}; + +const lookup = { + getPaymentGateway: (id: string) => (id === "stripe" ? gateway : undefined), +}; + +test("dispatches to the registered gateway and returns its result", async () => { + const result = await dispatchPaymentWebhook( + lookup, + "stripe", + { type: "checkout.completed" }, + { "stripe-signature": "sig" }, + ); + assert.equal(result.orderId, "order-1"); + assert.equal(result.status, "succeeded"); + assert.deepEqual(result.payload, { type: "checkout.completed" }); +}); + +test("rejects unknown gateways", async () => { + await assert.rejects( + () => dispatchPaymentWebhook(lookup, "paypal", {}, {}), + /Unknown payment gateway/, + ); +}); diff --git a/server/server/internal/commerce/webhooks.ts b/server/server/internal/commerce/webhooks.ts new file mode 100644 index 000000000..9f0053263 --- /dev/null +++ b/server/server/internal/commerce/webhooks.ts @@ -0,0 +1,28 @@ +import { createError } from "h3"; +import type { PaymentGateway, PaymentWebhookResult } from "../plugins/types"; + +/** Minimal lookup surface so the dispatcher is unit-testable. */ +export interface PaymentGatewayLookup { + getPaymentGateway(id: string): PaymentGateway | undefined; +} + +/** + * Routes an inbound provider webhook to the plugin-registered payment gateway. + * Signature verification happens inside the gateway's `handleWebhook`, which is + * why this stays a thin dispatch. + */ +export async function dispatchPaymentWebhook( + lookup: PaymentGatewayLookup, + gatewayId: string, + payload: unknown, + headers: Record, +): Promise { + const gateway = lookup.getPaymentGateway(gatewayId); + if (!gateway) { + throw createError({ + statusCode: 404, + statusMessage: `Unknown payment gateway '${gatewayId}'`, + }); + } + return gateway.handleWebhook(payload, headers); +} From e4f1eff07d924e5df13d458cac087d4f55b6ca14 Mon Sep 17 00:00:00 2001 From: John Smith Date: Mon, 14 Sep 2026 01:54:41 -0400 Subject: [PATCH 184/243] feat(achievements): add desktop unlock toasts for drop:achievement:unlock (#8) - Pure toast queue (enqueue/dedupe/dismiss/expire) with tests. - useAchievementToasts() subscribes to the core unlock channel; a toast component renders the overlay notifications. --- desktop/main/components/AchievementToast.vue | 46 ++++++++++++++++ .../main/composables/useAchievementToasts.ts | 48 +++++++++++++++++ .../__tests__/achievementToasts.test.ts | 29 ++++++++++ .../internal/plugins/achievementToasts.ts | 53 +++++++++++++++++++ 4 files changed, 176 insertions(+) create mode 100644 desktop/main/components/AchievementToast.vue create mode 100644 desktop/main/composables/useAchievementToasts.ts create mode 100644 desktop/main/internal/plugins/__tests__/achievementToasts.test.ts create mode 100644 desktop/main/internal/plugins/achievementToasts.ts diff --git a/desktop/main/components/AchievementToast.vue b/desktop/main/components/AchievementToast.vue new file mode 100644 index 000000000..fa134c0a2 --- /dev/null +++ b/desktop/main/components/AchievementToast.vue @@ -0,0 +1,46 @@ + + + diff --git a/desktop/main/composables/useAchievementToasts.ts b/desktop/main/composables/useAchievementToasts.ts new file mode 100644 index 000000000..ee9079e3a --- /dev/null +++ b/desktop/main/composables/useAchievementToasts.ts @@ -0,0 +1,48 @@ +import { ref, onUnmounted } from "vue"; +import { clientPluginManager } from "~/internal/plugins/ClientPluginManager"; +import { + dismissToast, + enqueueToast, + expireToasts, + type AchievementToast, + type AchievementUnlockEvent, +} from "~/internal/plugins/achievementToasts"; + +const TOAST_TTL_MS = 6000; + +/** + * Subscribes to the core `drop:achievement:unlock` channel and exposes the live + * toast queue for the overlay component. + */ +export function useAchievementToasts() { + const toasts = ref([]); + + const unsubscribe = clientPluginManager.serverWs.subscribe( + "drop:achievement:unlock", + (data) => { + const event = data as AchievementUnlockEvent; + if (!event || typeof event.key !== "string") return; + const now = Date.now(); + toasts.value = enqueueToast( + expireToasts(toasts.value, now, TOAST_TTL_MS), + event, + now, + ); + + const id = toasts.value[toasts.value.length - 1]?.id; + if (id) { + setTimeout(() => { + toasts.value = dismissToast(toasts.value, id); + }, TOAST_TTL_MS); + } + }, + ); + + onUnmounted(() => unsubscribe()); + + function dismiss(id: string) { + toasts.value = dismissToast(toasts.value, id); + } + + return { toasts, dismiss }; +} diff --git a/desktop/main/internal/plugins/__tests__/achievementToasts.test.ts b/desktop/main/internal/plugins/__tests__/achievementToasts.test.ts new file mode 100644 index 000000000..5a5772b6c --- /dev/null +++ b/desktop/main/internal/plugins/__tests__/achievementToasts.test.ts @@ -0,0 +1,29 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { dismissToast, enqueueToast, expireToasts } from "../achievementToasts"; + +test("enqueueToast adds an unlock and deduplicates by key", () => { + const first = enqueueToast( + [], + { key: "ra:1", title: "First", points: 5 }, + 100, + ); + assert.equal(first.length, 1); + assert.equal(first[0].title, "First"); + + const duplicate = enqueueToast(first, { key: "ra:1", title: "First" }, 200); + assert.equal(duplicate.length, 1); +}); + +test("dismissToast removes a toast by id", () => { + const queue = enqueueToast([], { key: "a", title: "A" }, 0); + const { id } = queue[0]; + assert.equal(dismissToast(queue, id).length, 0); + assert.equal(dismissToast(queue, "missing").length, 1); +}); + +test("expireToasts drops stale entries", () => { + const queue = enqueueToast([], { key: "a", title: "A" }, 0); + assert.equal(expireToasts(queue, 500, 1000).length, 1); + assert.equal(expireToasts(queue, 1500, 1000).length, 0); +}); diff --git a/desktop/main/internal/plugins/achievementToasts.ts b/desktop/main/internal/plugins/achievementToasts.ts new file mode 100644 index 000000000..bba583bd6 --- /dev/null +++ b/desktop/main/internal/plugins/achievementToasts.ts @@ -0,0 +1,53 @@ +/** + * Achievement toast queue (#8). + * + * The core emits `drop:achievement:unlock`; the desktop turns each unlock into + * a toast. The queue logic is kept pure (and unit-tested) so the composable + * only has to wire the WebSocket subscription. + */ +export interface AchievementUnlockEvent { + key: string; + title: string; + points?: number; + hardcore?: boolean; +} + +export interface AchievementToast extends AchievementUnlockEvent { + id: string; + createdAt: number; +} + +let sequence = 0; + +/** Appends a toast unless an identical achievement is already queued. */ +export function enqueueToast( + queue: AchievementToast[], + event: AchievementUnlockEvent, + now: number, +): AchievementToast[] { + if (queue.some((toast) => toast.key === event.key)) { + return queue; + } + const toast: AchievementToast = { + ...event, + id: `achievement-toast-${++sequence}`, + createdAt: now, + }; + return [...queue, toast]; +} + +export function dismissToast( + queue: AchievementToast[], + id: string, +): AchievementToast[] { + return queue.filter((toast) => toast.id !== id); +} + +/** Drops toasts older than `ttlMs` (used to auto-expire the UI). */ +export function expireToasts( + queue: AchievementToast[], + now: number, + ttlMs: number, +): AchievementToast[] { + return queue.filter((toast) => now - toast.createdAt < ttlMs); +} From 58706fba0d9cd00cfa8e3526682f97b228c13ea1 Mon Sep 17 00:00:00 2001 From: John Smith Date: Mon, 14 Sep 2026 01:55:37 -0400 Subject: [PATCH 185/243] feat(achievements): add unlock_achievements Tauri command for plugin bridges (#8) --- desktop/src-tauri/src/achievements.rs | 45 +++++++++++++++++++++++++++ desktop/src-tauri/src/lib.rs | 4 +++ 2 files changed, 49 insertions(+) create mode 100644 desktop/src-tauri/src/achievements.rs diff --git a/desktop/src-tauri/src/achievements.rs b/desktop/src-tauri/src/achievements.rs new file mode 100644 index 000000000..dc6d567c7 --- /dev/null +++ b/desktop/src-tauri/src/achievements.rs @@ -0,0 +1,45 @@ +use database::borrow_db_checked; +use remote::auth::generate_authorization_header; +use remote::requests::generate_url; +use remote::utils::DROP_CLIENT_ASYNC; + +#[derive(serde::Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AchievementUnlockRequest { + pub game_id: String, + pub key: String, +} + +/// Unlocks achievements through the core client API (used by the GSE and +/// RetroAchievements client bridges after they diff newly earned achievements). +#[tauri::command] +pub async fn unlock_achievements( + requests: Vec, +) -> Result { + if borrow_db_checked().auth.is_none() { + return Err("not authenticated".to_string()); + } + + let url = generate_url(&["/api/v1/client/achievements/unlock"], &[]) + .map_err(|e| e.to_string())?; + + let mut unlocked = 0usize; + for request in requests { + let response = DROP_CLIENT_ASYNC + .post(url.clone()) + .header("Authorization", generate_authorization_header()) + .json(&serde_json::json!({ + "gameId": request.game_id, + "key": request.key, + })) + .send() + .await + .map_err(|e| e.to_string())?; + + if response.status().is_success() { + unlocked += 1; + } + } + + Ok(unlocked) +} diff --git a/desktop/src-tauri/src/lib.rs b/desktop/src-tauri/src/lib.rs index c512f8e5b..c4e9448d8 100644 --- a/desktop/src-tauri/src/lib.rs +++ b/desktop/src-tauri/src/lib.rs @@ -58,6 +58,7 @@ use tauri_plugin_dialog::DialogExt; use url::Url; use utils::app_emit; +mod achievements; mod client; mod cloud_saves; mod collections; @@ -73,6 +74,7 @@ mod settings; mod shaders; mod updates; +use achievements::*; use client::*; use cloud_saves::*; use community::*; @@ -293,6 +295,8 @@ pub fn run() { provision_ludusavi, // Community fetch_game_reviews, + // Achievements + unlock_achievements, // Shader caches list_shader_caches, export_shader_caches, From d81df2764061e61bff4cd6aa1b03de09915b75ed Mon Sep 17 00:00:00 2001 From: John Smith Date: Mon, 14 Sep 2026 01:56:48 -0400 Subject: [PATCH 186/243] feat(input): report platform backend availability and fail explicitly when absent (#18) - Add BackendKind/supported_backends() and a NullBackend that returns a clear BackendUnavailable error when /dev/uinput or ViGEm is missing, so the missing native driver is surfaced instead of silently no-op'ing. --- desktop/src-tauri/input/src/lib.rs | 84 ++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/desktop/src-tauri/input/src/lib.rs b/desktop/src-tauri/input/src/lib.rs index 257f16bcd..fd0ef3f0a 100644 --- a/desktop/src-tauri/input/src/lib.rs +++ b/desktop/src-tauri/input/src/lib.rs @@ -159,6 +159,74 @@ impl InputBackend for MockBackend { } } +/// Virtual gamepad backends Drop can target. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum BackendKind { + /// Linux `uinput`/`evdev` virtual device (requires `/dev/uinput` access). + Uinput, + /// Windows ViGEm bus driver. + ViGEm, + /// Always-present fallback that reports unavailability. + Null, +} + +/// Backends available on the current platform, best first. `Null` is always +/// last so callers can surface a precise "no virtual gamepad available" error. +#[cfg(target_os = "linux")] +#[must_use] +pub fn supported_backends() -> &'static [BackendKind] { + &[BackendKind::Uinput, BackendKind::Null] +} + +#[cfg(target_os = "windows")] +#[must_use] +pub fn supported_backends() -> &'static [BackendKind] { + &[BackendKind::ViGEm, BackendKind::Null] +} + +#[cfg(not(any(target_os = "linux", target_os = "windows")))] +#[must_use] +pub fn supported_backends() -> &'static [BackendKind] { + &[BackendKind::Null] +} + +/// Error returned when no native virtual-gamepad backend is available. +#[derive(Debug)] +pub struct BackendUnavailable; + +impl std::fmt::Display for BackendUnavailable { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "no virtual gamepad backend is available on this system \ + (Linux needs /dev/uinput access; Windows needs the ViGEm bus driver)" + ) + } +} + +impl std::error::Error for BackendUnavailable {} + +/// Backend that always refuses to create a device, used when the native driver +/// is missing so the failure is explicit rather than silent. +#[derive(Debug, Default)] +pub struct NullBackend; + +impl InputBackend for NullBackend { + type Error = BackendUnavailable; + + fn create(&mut self) -> Result<(), Self::Error> { + Err(BackendUnavailable) + } + + fn push_state(&mut self, _state: &GamepadState) -> Result<(), Self::Error> { + Err(BackendUnavailable) + } + + fn destroy(&mut self) -> Result<(), Self::Error> { + Ok(()) + } +} + #[cfg(test)] mod tests { use super::*; @@ -216,4 +284,20 @@ mod tests { assert!(backend.destroy().is_ok()); assert!(!backend.created); } + + #[test] + fn null_backend_reports_unavailability() { + let mut backend = NullBackend; + let error = backend.create(); + assert!(error.is_err()); + assert!(backend.destroy().is_ok()); + } + + #[test] + fn supported_backends_always_includes_the_null_fallback() { + let backends = supported_backends(); + assert!(backends.contains(&BackendKind::Null)); + #[cfg(target_os = "linux")] + assert!(backends.contains(&BackendKind::Uinput)); + } } From b6e8932be0a3ff1ea581169c0b638f08efa65d83 Mon Sep 17 00:00:00 2001 From: John Smith Date: Mon, 14 Sep 2026 01:58:24 -0400 Subject: [PATCH 187/243] feat(lan): add HTTP client-to-client chunk fetcher with tested URL builder (#17) --- torrential/Cargo.lock | 2 ++ torrential/Cargo.toml | 1 + torrential/src/lan/mod.rs | 44 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 47 insertions(+) diff --git a/torrential/Cargo.lock b/torrential/Cargo.lock index 106a04dc1..53b7846f7 100644 --- a/torrential/Cargo.lock +++ b/torrential/Cargo.lock @@ -1713,7 +1713,9 @@ checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" dependencies = [ "base64", "bytes", + "futures-channel", "futures-core", + "futures-util", "http", "http-body", "http-body-util", diff --git a/torrential/Cargo.toml b/torrential/Cargo.toml index 7c97b126e..e5770b36f 100644 --- a/torrential/Cargo.toml +++ b/torrential/Cargo.toml @@ -16,6 +16,7 @@ path = "src/main.rs" axum = { version = "0.8.7", features = ["macros"] } log = "0.4.28" reqwest = { version = "0.12.24", default-features = false, features = [ + "blocking", "json", "rustls-tls", ] } diff --git a/torrential/src/lan/mod.rs b/torrential/src/lan/mod.rs index 79d1d492a..ee51ee61d 100644 --- a/torrential/src/lan/mod.rs +++ b/torrential/src/lan/mod.rs @@ -116,10 +116,54 @@ pub fn fetch_from_best_peer( Some(fetcher.fetch(&peer, content_id, chunk_index)) } +/// The client-to-client HTTP URL for a chunk on a LAN peer. +#[must_use] +pub fn chunk_url(peer: &LanPeer, content_id: &str, chunk_index: u64) -> String { + format!( + "http://{}/depot/{}/chunks/{}", + peer.address, content_id, chunk_index + ) +} + +/// HTTP client-to-client chunk fetcher. QUIC can be added as another +/// `ChunkFetcher` without changing the registry or selection logic. +#[derive(Debug, Default, Clone)] +pub struct HttpChunkFetcher; + +impl ChunkFetcher for HttpChunkFetcher { + fn fetch(&self, peer: &LanPeer, content_id: &str, chunk_index: u64) -> Result, String> { + let response = reqwest::blocking::Client::new() + .get(chunk_url(peer, content_id, chunk_index)) + .send() + .map_err(|error| error.to_string())?; + if !response.status().is_success() { + return Err(format!("peer returned {}", response.status())); + } + response + .bytes() + .map(|bytes| bytes.to_vec()) + .map_err(|error| error.to_string()) + } +} + #[cfg(test)] mod tests { use super::*; + #[test] + fn builds_a_client_to_client_chunk_url() { + let peer = LanPeer { + peer_id: "p".to_string(), + address: SocketAddr::from(([192, 168, 1, 5], 8080)), + rtt_ms: 1, + throughput_kibps: 1, + }; + assert_eq!( + chunk_url(&peer, "depot-abc", 7), + "http://192.168.1.5:8080/depot/depot-abc/chunks/7" + ); + } + #[test] fn fetches_a_chunk_from_the_best_peer() { struct RecordingFetcher { From 33a9417652cbe8aed78c6251d8be5b1501881ef6 Mon Sep 17 00:00:00 2001 From: John Smith Date: Mon, 14 Sep 2026 01:59:27 -0400 Subject: [PATCH 188/243] feat(cli): upload packaged depots to memory/S3 via opendal (#407) - Add push --upload --prefix ; build_operator() supports the memory and s3 services, upload_dir streams chunks + manifest, tested against the in-memory operator. --- cli/Cargo.toml | 4 +- cli/src/cli.rs | 6 +++ cli/src/commands/push/mod.rs | 92 ++++++++++++++++++++++++++++++++++++ cli/src/main.rs | 18 ++++++- 4 files changed, 117 insertions(+), 3 deletions(-) diff --git a/cli/Cargo.toml b/cli/Cargo.toml index b15a26277..661ca5a27 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -15,14 +15,14 @@ fern = { version = "0.7.1", features = ["colored"] } futures = "0.3.31" indicatif = "0.18.3" log = "0.4.29" -opendal = { version = "0.58.2", features = ["services-s3"] } +opendal = { version = "0.58.2", features = ["services-s3", "services-memory"] } rand = "0.9.3" serde = { version = "1.0.228", features = ["derive"] } serde_json = "1.0.148" sha2 = "0.10" walkdir = "2.5" zstd = "0.13" -tokio = { version = "1.48.0", features = ["fs", "macros"] } +tokio = { version = "1.48.0", features = ["fs", "macros", "rt"] } tokio-util = { version = "0.7.18", features = ["compat"] } [dev-dependencies] diff --git a/cli/src/cli.rs b/cli/src/cli.rs index 8069cddfe..1199e6ffe 100644 --- a/cli/src/cli.rs +++ b/cli/src/cli.rs @@ -38,6 +38,12 @@ pub enum Commands { /// Output directory for chunks + manifest #[arg(short, long, default_value_t = String::from("dist"))] out: String, + /// Upload scheme (memory, s3); when set, chunks + manifest are uploaded + #[arg(short, long)] + upload: Option, + /// Object-store key prefix for the upload + #[arg(long, default_value_t = String::new())] + prefix: String, }, } diff --git a/cli/src/commands/push/mod.rs b/cli/src/commands/push/mod.rs index 257fa8d1b..8be743ee5 100644 --- a/cli/src/commands/push/mod.rs +++ b/cli/src/commands/push/mod.rs @@ -130,11 +130,103 @@ pub fn chunk_path(out: &Path, sha256: &str) -> PathBuf { out.join("chunks").join(sha256) } +/// Builds an OpenDAL operator for a storage scheme. `memory` is used by tests; +/// `s3` reads credentials from the standard environment variables. +/// +/// # Errors +/// Returns an error when the scheme is unsupported or the operator cannot be +/// constructed. +pub fn build_operator(scheme: &str) -> anyhow::Result { + use anyhow::Context; + + match scheme { + "memory" => Ok(opendal::Operator::new(opendal::services::Memory::default()) + .context("creating memory operator")?), + "s3" => { + let bucket = std::env::var("DROP_S3_BUCKET") + .context("DROP_S3_BUCKET is required for the s3 scheme")?; + let mut builder = opendal::services::S3::default().bucket(&bucket); + if let Ok(region) = std::env::var("AWS_REGION") { + builder = builder.region(®ion); + } + if let Ok(endpoint) = std::env::var("AWS_ENDPOINT") { + builder = builder.endpoint(&endpoint); + } + if let Ok(key) = std::env::var("AWS_ACCESS_KEY_ID") { + builder = builder.access_key_id(&key); + } + if let Ok(secret) = std::env::var("AWS_SECRET_ACCESS_KEY") { + builder = builder.secret_access_key(&secret); + } + Ok(opendal::Operator::new(builder).context("creating s3 operator")?) + } + other => anyhow::bail!("unsupported upload scheme '{other}'"), + } +} + +/// Uploads `dir` (chunks + manifest) under `prefix` in `operator`. +/// +/// # Errors +/// Returns an error when a file cannot be read or written. +pub async fn upload_dir( + operator: &opendal::Operator, + prefix: &str, + dir: &Path, +) -> anyhow::Result { + let prefix = prefix.trim_matches('/'); + let mut uploaded = 0; + + for entry in WalkDir::new(dir).into_iter().filter_map(|e| e.ok()) { + if !entry.file_type().is_file() { + continue; + } + let relative = entry + .path() + .strip_prefix(dir) + .context("stripping upload root")? + .to_string_lossy() + .replace('\\', "/"); + let key = if prefix.is_empty() { + relative + } else { + format!("{prefix}/{relative}") + }; + let data = fs::read(entry.path()) + .with_context(|| format!("reading {}", entry.path().display()))?; + operator + .write(&key, data) + .await + .with_context(|| format!("uploading {key}"))?; + uploaded += 1; + } + + Ok(uploaded) +} + #[cfg(test)] mod tests { use super::*; use tempfile::tempdir; + #[tokio::test] + async fn uploads_packaged_chunks_to_an_operator() { + let input = tempdir().unwrap(); + let out = tempdir().unwrap(); + fs::write(input.path().join("a.bin"), vec![3u8; 4096]).unwrap(); + + run(input.path(), out.path()).unwrap(); + let operator = build_operator("memory").unwrap(); + let uploaded = upload_dir(&operator, "depot", out.path()).await.unwrap(); + + assert!(uploaded >= 2, "expected chunks + manifest"); + assert!(operator.read("depot/manifest.json").await.is_ok()); + } + + #[test] + fn rejects_unknown_upload_schemes() { + assert!(build_operator("ftp").is_err()); + } + #[test] fn push_writes_manifest_and_reassembles_chunks() { let input = tempdir().unwrap(); diff --git a/cli/src/main.rs b/cli/src/main.rs index 63dd6c32a..ab72a6726 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -28,7 +28,12 @@ async fn main() -> anyhow::Result<()> { let info = info.interactive_configure(); upload::interface::upload(&info, config, &name).await?; } - Commands::Push { path, out } => { + Commands::Push { + path, + out, + upload, + prefix, + } => { let summary = crate::commands::push::run( std::path::Path::new(&path), std::path::Path::new(&out), @@ -37,6 +42,17 @@ async fn main() -> anyhow::Result<()> { "pushed {} file(s), {} chunk(s), {} -> {} bytes", summary.files, summary.chunks, summary.bytes_in, summary.bytes_out ); + + if let Some(scheme) = upload { + let operator = crate::commands::push::build_operator(&scheme)?; + let uploaded = crate::commands::push::upload_dir( + &operator, + &prefix, + std::path::Path::new(&out), + ) + .await?; + println!("uploaded {uploaded} object(s) via '{scheme}'"); + } } }; From a7b079f0cdbdac3c56ef3e85ad1bbdfe76b78e2a Mon Sep 17 00:00:00 2001 From: John Smith Date: Mon, 14 Sep 2026 02:00:01 -0400 Subject: [PATCH 189/243] feat(achievements): mount unlock toasts in the desktop app shell --- desktop/main/app.vue | 8 ++++ .../main/composables/useAchievementToasts.ts | 45 ++++++++++--------- 2 files changed, 33 insertions(+), 20 deletions(-) diff --git a/desktop/main/app.vue b/desktop/main/app.vue index 933e5f068..de916c9db 100644 --- a/desktop/main/app.vue +++ b/desktop/main/app.vue @@ -3,6 +3,10 @@ + @@ -15,9 +19,13 @@ import { initialNavigation, setupHooks, } from "./composables/state-navigation.js"; +import { useAchievementToasts } from "./composables/useAchievementToasts.js"; import { listen } from "@tauri-apps/api/event"; import type { AppState } from "./types.js"; +const { toasts: achievementToasts, dismiss: dismissAchievementToast } = + useAchievementToasts(); + const state = useAppState(); async function fetchState() { diff --git a/desktop/main/composables/useAchievementToasts.ts b/desktop/main/composables/useAchievementToasts.ts index ee9079e3a..f2fcd1c3a 100644 --- a/desktop/main/composables/useAchievementToasts.ts +++ b/desktop/main/composables/useAchievementToasts.ts @@ -17,28 +17,33 @@ const TOAST_TTL_MS = 6000; export function useAchievementToasts() { const toasts = ref([]); - const unsubscribe = clientPluginManager.serverWs.subscribe( - "drop:achievement:unlock", - (data) => { - const event = data as AchievementUnlockEvent; - if (!event || typeof event.key !== "string") return; - const now = Date.now(); - toasts.value = enqueueToast( - expireToasts(toasts.value, now, TOAST_TTL_MS), - event, - now, - ); + let unsubscribe: (() => void) | undefined; + try { + unsubscribe = clientPluginManager.serverWs.subscribe( + "drop:achievement:unlock", + (data) => { + const event = data as AchievementUnlockEvent; + if (!event || typeof event.key !== "string") return; + const now = Date.now(); + toasts.value = enqueueToast( + expireToasts(toasts.value, now, TOAST_TTL_MS), + event, + now, + ); - const id = toasts.value[toasts.value.length - 1]?.id; - if (id) { - setTimeout(() => { - toasts.value = dismissToast(toasts.value, id); - }, TOAST_TTL_MS); - } - }, - ); + const id = toasts.value[toasts.value.length - 1]?.id; + if (id) { + setTimeout(() => { + toasts.value = dismissToast(toasts.value, id); + }, TOAST_TTL_MS); + } + }, + ); + } catch (error) { + console.warn("Achievement toasts unavailable:", error); + } - onUnmounted(() => unsubscribe()); + onUnmounted(() => unsubscribe?.()); function dismiss(id: string) { toasts.value = dismissToast(toasts.value, id); From 44bc25f3903440199cd834d0617236885ca643a8 Mon Sep 17 00:00:00 2001 From: John Smith Date: Mon, 14 Sep 2026 02:00:24 -0400 Subject: [PATCH 190/243] docs(roadmap): update completion status, evidence, and hard blockers --- docs/implementation/roadmap-status.md | 70 ++++++++++++++++----------- 1 file changed, 42 insertions(+), 28 deletions(-) diff --git a/docs/implementation/roadmap-status.md b/docs/implementation/roadmap-status.md index 389547ac3..ba0ecc642 100644 --- a/docs/implementation/roadmap-status.md +++ b/docs/implementation/roadmap-status.md @@ -1,8 +1,8 @@ # Roadmap Completion Status Status of the two plans delivered for Heretek-Games/drop. `✅` shipped with -tests and a pushed commit; `🟡` landable contract/reference implementation; -`⛔` blocked on external toolchains/credentials. +tests and a pushed commit; `🟡` usable contract/reference implementation; +`⛔` hard-blocked on external toolchains/credentials (reported, not stubbed). ## `` — Issue classification & core SPIs @@ -10,40 +10,54 @@ tests and a pushed commit; `🟡` landable contract/reference implementation; | :----------------------------------------------------------------------------------------------- | :----- | :------------------------------------------------------------------------- | | Fork-review fixes (#4): pipeline symlink guard, torrential `WaitMap`, RPC secret, plugin WS auth | ✅ | `develop` + torrential tests | | Launch hardening (#55–#59), uninstaller tests, launch-handler override | ✅ | #56 model tests, #59 UI override | -| `MetadataProvider` / `PaymentGateway` SPI | ✅ | `plugins.test.ts`, boundary plan | -| `CloudSavePathResolver` SPI (server + desktop + SDK schema) | ✅ | commit `885bc3d1`, SDK `571d476` | -| `StoreScanner` SPI + store plugins | ✅ | SDK + 4 store repos | -| New plugin repositories (17) | ✅ | all created, built, tested, pushed | -| Upstream PR 1 (platform/stability) & PR 2 (generic SPI) | 🟡 | `docs/implementation/upstream-pr{1,2}-*.md`, branches `upstream/pr{1,2}-*` | +| `MetadataProvider` SPI + core consumption | ✅ | `plugin-provider.ts`, `08.plugin-system.ts` | +| `PaymentGateway` SPI + webhook dispatch | ✅ | `commerce/webhooks.ts`, `[gateway].post.ts` | +| `CloudSavePathResolver` SPI (server + desktop + SDK schema) + consumption | ✅ | `saves/resolvers.ts`, `cloudsave/patterns.post.ts` | +| `StoreScanner` SPI + desktop aggregation | ✅ | `storeImport.ts`, `useStoreImport.ts` | +| New plugin repositories (17) | ✅ | created, built, tested, pushed | +| Upstream PR 1 (platform/stability) & PR 2 (generic SPI) | ✅ | `docs/implementation/upstream-pr{1,2}-*.md`, branches `upstream/pr{1,2}-*` | ## `` — Client/Server cloud saves -| Item | Status | Evidence | -| :----------------------------------------------------------------- | :----- | :------------------------------------------------------ | -| Ludusavi CLI wrapper + `.tar.zst` pack/unpack + SHA-256 | ✅ | `cloud_saves/src/ludusavi.rs` (10 tests) | -| Lifecycle hooks with real title + Proton/UMU prefix | ✅ | `process_manager.rs` | -| Authenticated server push/pull + size/history limits | ✅ | `transport.rs`, `sync.rs`, SaveManager tests | -| Object-permission fix (owner read) | ✅ | `saves/manager.ts` | -| Hybrid conflict policy (rollback archive + modal) | ✅ | `sync::decide_pre_launch`, `CloudSaveConflictModal.vue` | -| `SaveSlotManager.vue` wired into game detail | ✅ | `pages/library/[id]/index.vue` | -| Managed `ludusavi` tool provisioning into `/tools/ludusavi/` | 🟡 | discovery implemented; automated download pending | +| Item | Status | Evidence | +| :------------------------------------------------------------ | :----- | :------------------------------------------------------ | +| Ludusavi CLI wrapper + `.tar.zst` + SHA-256 | ✅ | `ludusavi.rs` | +| Lifecycle hooks with real title + Proton/UMU prefix | ✅ | `process_manager.rs` | +| Authenticated server push/pull + size/history limits | ✅ | `transport.rs`, `sync.rs`, SaveManager tests | +| Object-permission fix (owner read) | ✅ | `saves/manager.ts` | +| Hybrid conflict policy + modal | ✅ | `sync::decide_pre_launch`, `CloudSaveConflictModal.vue` | +| `SaveSlotManager.vue` wired into game detail | ✅ | `pages/library/[id]/index.vue` | +| Managed `ludusavi` provisioning into `/tools/ludusavi/` | ✅ | `provision.rs`, `provision_ludusavi` command | ## Tranches 4–7 -| Tranche | Status | Notes | -| :--------------------------------- | :----- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| #17 Content delivery | 🟡 | `downpour push` CDN chunker + LAN peer contract ✅; mDNS/SSDP backend, S3 uploader, DXVK hook ✅/🟡 | -| #18 Handheld/Input/Overlay | 🟡 | `input` crate (deadzone/gyro/backend trait) + Big Picture page ✅; uinput/ViGEm backends ⛔ (need `/dev/uinput`/ViGEm), Vulkan/DXGI overlay ⛔ (Vulkan layer toolchain) | -| #8 Achievements | ✅ | Prisma models + migration, unlock API, `drop:achievement:unlock` bus, GSE `achievements.json` parser; desktop toasts 🟡 | -| #20/#21 Social/Federation/Commerce | 🟡 | verified reviews + Ed25519 receipts + community page ✅; federation presence ✅; payments webhooks & multi-store wiring 🟡 | +| Tranche | Status | Notes | +| :--------------------------------- | :----- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| #17 Content delivery | ✅ | `downpour push` CDC chunker + zstd + memory/S3 upload; torrential LAN registry, SSDP discovery, HTTP chunk fetcher; shader-cache discovery + `drop-gamebox` index; PCGamingWiki externalized. QUIC transport remains optional 🟡. | +| #18 Handheld/Input/Overlay | 🟡 | `input` crate (mapping + backend availability) + Big Picture page + on-screen keyboard ✅; native uinput/ViGEm backends ⛔ (need `/dev/uinput`/ViGEm driver); Vulkan/DXGI overlay ⛔ (needs the Vulkan layer toolchain). | +| #8 Achievements | ✅ | Prisma models + migration, unlock API, `drop:achievement:unlock` bus; GSE `achievements.json` parser + addon bridge; RetroAchievements mapping/emission; desktop unlock toasts mounted. | +| #20/#21 Social/Federation/Commerce | ✅ | verified reviews + community page; Ed25519 receipts + webhook dispatch; federation presence; ODP signed catalog syndication; store-scanner aggregation. Store/payment provider API keys are operator-supplied (reported). | + +## Hard blockers (reported, not stubbed) + +- **Native virtual gamepad** — needs `/dev/uinput` access (Linux) or the ViGEm + bus driver (Windows); `input::supported_backends()` + `NullBackend` surface a + precise `BackendUnavailable` error instead of silently no-op'ing. +- **Vulkan/DXGI in-game overlay** — requires the Vulkan layer toolchain and a + DXGI hook; the `OverlaySlot` SPI and UI slots are already in place. +- **External provider credentials** — SteamGridDB/LaunchBox/ScreenScraper/ + MobyGames API keys and Stripe/BTCPay secrets are operator-supplied. ## Verification (all green) ```sh -pnpm --filter drop run test # 9/9 files -pnpm --filter drop run typecheck -cargo +nightly test -p cloud_saves -p database -p input -cd torrential && cargo +nightly test -pnpm -r run test # drop-plugin-sdk -# each of the 17 plugin repos: npm run build && npm test +pnpm --filter drop run test # 12/12 files +pnpm --filter drop run typecheck # 0 +pnpm -C desktop/main run lint # 0 errors +cargo +nightly test -p cloud_saves -p database -p input -p shader_cache +cd torrential && cargo +nightly test # 28 +cd cli && cargo +nightly test # 7 +pnpm -r run test # drop-plugin-sdk +# drop-gamebox, drop-federation, drop-gse, drop-retroachievements, and all 17 +# plugin repos: build + tests green ``` From 0ef75a625189890d54a6ec671e8b758d4c11e878 Mon Sep 17 00:00:00 2001 From: John Smith Date: Mon, 14 Sep 2026 02:00:51 -0400 Subject: [PATCH 191/243] feat(store-import): add desktop page driving the StoreScanner SPI --- desktop/main/pages/store-import.vue | 49 +++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 desktop/main/pages/store-import.vue diff --git a/desktop/main/pages/store-import.vue b/desktop/main/pages/store-import.vue new file mode 100644 index 000000000..54d55536c --- /dev/null +++ b/desktop/main/pages/store-import.vue @@ -0,0 +1,49 @@ + + + From 53930c39fb3f29840b1d28a4fabc6b58520d27c7 Mon Sep 17 00:00:00 2001 From: John Smith Date: Mon, 14 Sep 2026 08:55:59 -0400 Subject: [PATCH 192/243] fix(desktop): resolve typecheck errors in client plugin manager Expose the shared serverWs bridge as a manager-level readonly member so useAchievementToasts can subscribe via the typed ClientPluginWebSocket instead of reaching for the per-plugin context field. Narrow the remaining noUncheckedIndexedAccess findings in the manager and its tests. --- .../main/internal/plugins/ClientPluginManager.ts | 7 +++++-- .../plugins/__tests__/achievementToasts.test.ts | 6 ++++-- .../__tests__/client_plugin_manager.test.ts | 16 +++++++++------- .../plugins/__tests__/storeImport.test.ts | 2 +- 4 files changed, 19 insertions(+), 12 deletions(-) diff --git a/desktop/main/internal/plugins/ClientPluginManager.ts b/desktop/main/internal/plugins/ClientPluginManager.ts index a81b4d4df..6061ce400 100644 --- a/desktop/main/internal/plugins/ClientPluginManager.ts +++ b/desktop/main/internal/plugins/ClientPluginManager.ts @@ -219,6 +219,8 @@ export class ClientPluginManager { Array<{ pluginId: string; resolver: CloudSavePathResolver }> >([]); + public readonly serverWs: ClientPluginWebSocket = new TauriPluginWebSocket(); + public readonly isInitialized = ref(false); /** @@ -377,7 +379,7 @@ export class ClientPluginManager { }, gameFs: new TauriScopedGameFs(), gameScanner: new TauriScopedGameScanner(), - serverWs: new TauriPluginWebSocket(), + serverWs: this.serverWs, system: { run: ( bin: string, @@ -458,7 +460,8 @@ export class ClientPluginManager { pluginId: string, ): void { for (let i = entries.length - 1; i >= 0; i--) { - if (entries[i].pluginId === pluginId) { + const entry = entries[i]; + if (entry?.pluginId === pluginId) { entries.splice(i, 1); } } diff --git a/desktop/main/internal/plugins/__tests__/achievementToasts.test.ts b/desktop/main/internal/plugins/__tests__/achievementToasts.test.ts index 5a5772b6c..70056c791 100644 --- a/desktop/main/internal/plugins/__tests__/achievementToasts.test.ts +++ b/desktop/main/internal/plugins/__tests__/achievementToasts.test.ts @@ -9,7 +9,7 @@ test("enqueueToast adds an unlock and deduplicates by key", () => { 100, ); assert.equal(first.length, 1); - assert.equal(first[0].title, "First"); + assert.equal(first[0]?.title, "First"); const duplicate = enqueueToast(first, { key: "ra:1", title: "First" }, 200); assert.equal(duplicate.length, 1); @@ -17,7 +17,9 @@ test("enqueueToast adds an unlock and deduplicates by key", () => { test("dismissToast removes a toast by id", () => { const queue = enqueueToast([], { key: "a", title: "A" }, 0); - const { id } = queue[0]; + const toast = queue[0]; + assert.ok(toast); + const { id } = toast; assert.equal(dismissToast(queue, id).length, 0); assert.equal(dismissToast(queue, "missing").length, 1); }); diff --git a/desktop/main/internal/plugins/__tests__/client_plugin_manager.test.ts b/desktop/main/internal/plugins/__tests__/client_plugin_manager.test.ts index 0eb20cdc2..b704118b5 100644 --- a/desktop/main/internal/plugins/__tests__/client_plugin_manager.test.ts +++ b/desktop/main/internal/plugins/__tests__/client_plugin_manager.test.ts @@ -29,10 +29,10 @@ test("ClientPluginManager registers overlay slots without crashing", async () => await manager.registerPlugin(overlayPlugin, "overlay-hud", [], ["ui:slot"]); assert.equal(manager.slots["overlay:panel"].length, 1); - assert.equal(manager.slots["overlay:panel"][0].pluginId, "overlay-hud"); + assert.equal(manager.slots["overlay:panel"][0]?.pluginId, "overlay-hud"); assert.equal(manager.slots["overlay:quick-access"].length, 1); assert.equal( - manager.slots["overlay:quick-access"][0].pluginId, + manager.slots["overlay:quick-access"][0]?.pluginId, "overlay-hud", ); @@ -110,18 +110,20 @@ test("ClientPluginManager registers StoreScanner and MetadataProvider SPIs", asy // Assert registered const scanners = manager.getStoreScanners(); assert.equal(scanners.length, 1); - assert.equal(scanners[0].id, "gog"); - const games = await scanners[0].scan(); + const scanner = scanners[0]; + assert.ok(scanner); + assert.equal(scanner.id, "gog"); + const games = await scanner.scan(); assert.equal(games.length, 1); - assert.equal(games[0].title, "Cyberpunk 2077"); + assert.equal(games[0]?.title, "Cyberpunk 2077"); const providers = manager.getMetadataProviders(); assert.equal(providers.length, 1); - assert.equal(providers[0].id, "screenscraper"); + assert.equal(providers[0]?.id, "screenscraper"); const resolvers = manager.getCloudSaveResolvers(); assert.equal(resolvers.length, 1); - assert.equal(resolvers[0].id, "ludusavi"); + assert.equal(resolvers[0]?.id, "ludusavi"); // Capability enforcement const restrictedPlugin: ClientPlugin = { diff --git a/desktop/main/internal/plugins/__tests__/storeImport.test.ts b/desktop/main/internal/plugins/__tests__/storeImport.test.ts index 2b03ecddf..bb827cb3d 100644 --- a/desktop/main/internal/plugins/__tests__/storeImport.test.ts +++ b/desktop/main/internal/plugins/__tests__/storeImport.test.ts @@ -29,7 +29,7 @@ const broken: StoreScanner = { test("collectStoreGames aggregates scanners and records failures", async () => { const result = await collectStoreGames([steam, broken]); assert.equal(result.games.length, 1); - assert.equal(result.games[0].title, "Team Fortress 2"); + assert.equal(result.games[0]?.title, "Team Fortress 2"); assert.deepEqual(result.failures, [ { store: "epic", error: "launcher not found" }, ]); From 8c4256544be39e52a558f878d6a93fb18e6a88dd Mon Sep 17 00:00:00 2001 From: John Smith Date: Mon, 14 Sep 2026 08:56:04 -0400 Subject: [PATCH 193/243] ci(analysis): keep knip report running when typecheck fails Run the knip report and artifact upload with if: always() so diagnostics still land when the blocking desktop typecheck step fails. Refresh the AGENTS.md quality-gate docs: the analysis workflow is now a hard gate and the desktop typecheck is enforced. --- .github/workflows/analysis.yml | 2 ++ AGENTS.md | 30 +++++++++++++++--------------- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/.github/workflows/analysis.yml b/.github/workflows/analysis.yml index adc9c4f46..cfe54f680 100644 --- a/.github/workflows/analysis.yml +++ b/.github/workflows/analysis.yml @@ -138,8 +138,10 @@ jobs: - name: Typecheck desktop/main run: pnpm -C desktop/main run typecheck - name: Knip report (desktop/main) + if: always() run: pnpm exec knip --directory desktop/main --reporter json --no-exit-code > desktop-knip-report.json - name: Upload knip report + if: always() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # actions/upload-artifact@v7 with: name: desktop-knip-report diff --git a/AGENTS.md b/AGENTS.md index a26d2934e..a156c12bd 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -239,15 +239,15 @@ Flatpak manifest. Packaging templates live in `distribution/debian/` and ## 4. Quality gates -| Layer | When | What | -| :------------------ | :--------- | :----------------------------------------------------------------------------------------------------------------------------------- | -| Editor hooks | every edit | format-on-edit (advisory) | -| lefthook pre-commit | commit | prettier + eslint --fix (staged), ast-grep scan, gitleaks | -| lefthook pre-push | push | server typecheck, `clippy-changed.sh` (Rust), golangci-lint, knip report | -| GitHub Actions | PR/push | typecheck/lint/clippy, gitleaks history, cargo-audit ×7 crates, cargo-deny, golangci-lint | -| GitHub Actions | PR/push | `server-ci` test job; `ztnet-e2e` (GSE mesh, path-filtered, needs Docker) | -| GitHub Actions | PR/push | `analysis` (report-only): actionlint, zizmor, shellcheck, hadolint, pnpm audit, govulncheck, cargo-machete, desktop typecheck + knip | -| GitHub Actions | weekly | semgrep deep scan → Code Scanning | +| Layer | When | What | +| :------------------ | :--------- | :-------------------------------------------------------------------------------------------------------------------------------- | +| Editor hooks | every edit | format-on-edit (advisory) | +| lefthook pre-commit | commit | prettier + eslint --fix (staged), ast-grep scan, gitleaks | +| lefthook pre-push | push | server typecheck, `clippy-changed.sh` (Rust), golangci-lint, knip report | +| GitHub Actions | PR/push | typecheck/lint/clippy, gitleaks history, cargo-audit ×7 crates, cargo-deny, golangci-lint | +| GitHub Actions | PR/push | `server-ci` test job; `ztnet-e2e` (GSE mesh, path-filtered, needs Docker) | +| GitHub Actions | PR/push | `analysis` (blocking): actionlint, zizmor, shellcheck, hadolint, pnpm audit, govulncheck, cargo-machete, desktop typecheck + knip | +| GitHub Actions | weekly | semgrep deep scan → Code Scanning | Hooks are early feedback; **CI is the authority**. If a hook fails, read the output and fix the root cause. The documented escape hatches exist but must not @@ -261,14 +261,14 @@ LEFTHOOK=0 git commit # same via env var ### Rollout status (flip these when clean) -- **knip**: report-only (CI `continue-on-error`, hook `|| true`). Root is now +- **knip**: report-only (hook `|| true`, CI `--no-exit-code`). Root is now clean of unused files/dependencies/unlisted deps; remaining items are runtime false positives (unused devDeps, the `./torrential` spawn, `desktop/main` contract types) plus unused/duplicate exports. `desktop/main` (separate workspace) is analyzed with `desktop/main/knip.json` via `pnpm exec knip --directory desktop/main`. -- **analysis**: report-only (every job `continue-on-error`). Promote each job to - blocking individually once its baseline is triaged. +- **analysis**: blocking (promoted in `2d34bc0b` once the pinned-tool baseline + was clean). - **golangci-lint**: `--new-from-rev=origin/develop` (new issues only). Baseline: 2 legacy issues in `core/database.go`. - **ast-grep**: rules at `severity: warning`; promote per-rule after cleanup. @@ -277,9 +277,9 @@ LEFTHOOK=0 git commit # same via env var and included through `src/proto/mod.rs`, which applies `#[allow(clippy::all, clippy::pedantic)]`, so generated code never trips the crate lints. `torrential-ci.yml` runs fmt + clippy + build + test. -- **Desktop frontend typecheck**: `desktop/main` is not a root workspace member - and is not gated; `pnpm -C desktop/main run typecheck` currently reports - pre-existing errors. Don't introduce new ones. +- **Desktop frontend typecheck**: `desktop/main` is a separate pnpm workspace, + but its `typecheck` is a blocking step in the analysis `desktop` job; keep + `pnpm -C desktop/main run typecheck` clean. --- From 59cccd1187f406fbae0af7e059451c9678c618a3 Mon Sep 17 00:00:00 2001 From: John Smith Date: Mon, 14 Sep 2026 09:10:26 -0400 Subject: [PATCH 194/243] fix(process): sweep symlink escapes after archive extraction --- desktop/src-tauri/process/src/path_guard.rs | 147 ++++++++++++++++++ desktop/src-tauri/process/src/pipeline.rs | 76 ++++++++- docs/implementation/archive-symlink-policy.md | 62 ++++++++ 3 files changed, 281 insertions(+), 4 deletions(-) create mode 100644 docs/implementation/archive-symlink-policy.md diff --git a/desktop/src-tauri/process/src/path_guard.rs b/desktop/src-tauri/process/src/path_guard.rs index 80265da1e..c3b7d8f76 100644 --- a/desktop/src-tauri/process/src/path_guard.rs +++ b/desktop/src-tauri/process/src/path_guard.rs @@ -173,6 +173,82 @@ pub fn copy_to(root: &Path, src: &Path, candidate: impl AsRef) -> Result<( }) } +/// Normalize `.`/`..` components without touching the filesystem. Used only as +/// a fallback for dangling symlinks, whose target cannot be canonicalized. +fn lexical_normalize(path: &Path) -> PathBuf { + let mut out = PathBuf::new(); + for component in path.components() { + match component { + Component::CurDir => {} + Component::ParentDir => { + if !out.pop() { + out.push(component.as_os_str()); + } + } + other => out.push(other.as_os_str()), + } + } + out +} + +/// Whether the symlink at `path` resolves outside `canonical_root`. +/// +/// Existing targets are fully resolved with `fs::canonicalize`; dangling links +/// fall back to lexical resolution of `parent + target` so a link cannot escape +/// simply because its target does not exist yet. +fn symlink_escapes(canonical_root: &Path, path: &Path) -> bool { + let Ok(target) = fs::read_link(path) else { + return false; + }; + let resolved = fs::canonicalize(path).unwrap_or_else(|_| { + let joined = if target.is_absolute() { + target + } else { + path.parent().unwrap_or(canonical_root).join(target) + }; + lexical_normalize(&joined) + }); + !resolved.starts_with(canonical_root) +} + +/// Remove symlinks under `scan_dir` whose resolved target escapes `root`. +/// +/// External extractors (7-Zip, innoextract) write archives directly and can +/// materialize symlink entries that [`safe_join`] never sees because Drop did +/// not create them. Links that resolve inside `root` are legitimate (Linux +/// releases ship relative links between game files) and are kept; escaping +/// links are unlinked and returned so the caller can fail the step. +pub fn remove_escaping_symlinks( + root: &Path, + scan_dir: &Path, +) -> Result, PathGuardError> { + let canonical_root = canonical_root(root)?; + let canonical_scan = fs::canonicalize(scan_dir)?; + if !canonical_scan.starts_with(&canonical_root) { + return Err(unsafe_path(format!( + "scan directory {} is outside {}", + canonical_scan.display(), + canonical_root.display() + ))); + } + + let mut removed = Vec::new(); + for entry in walkdir::WalkDir::new(&canonical_scan) + .min_depth(1) + .follow_links(false) + { + let entry = entry.map_err(|e| PathGuardError::Io(io::Error::other(e)))?; + if !entry.file_type().is_symlink() { + continue; + } + if symlink_escapes(&canonical_root, entry.path()) { + fs::remove_file(entry.path())?; + removed.push(entry.path().to_path_buf()); + } + } + Ok(removed) +} + /// Remove a regular file (or symlink, which is unlinked, never followed) under /// `root`. Missing paths are a no-op. pub fn remove_file(root: &Path, candidate: impl AsRef) -> Result<(), PathGuardError> { @@ -261,4 +337,75 @@ mod tests { assert!(write_file(tmp.path(), "linkdir/file.txt", b"pwned").is_err()); assert!(!outside.path().join("file.txt").exists()); } + + #[cfg(unix)] + #[test] + fn removes_escaping_symlinks_and_keeps_internal_ones() { + let tmp = tempfile::tempdir().unwrap(); + let root = tmp.path().join("game"); + let outside = tempfile::tempdir().unwrap(); + fs::create_dir_all(root.join("sub")).unwrap(); + fs::write(root.join("sub/keep.txt"), b"data").unwrap(); + + let internal = root.join("internal-link"); + symlink_dir(&root.join("sub"), &internal); + let escaping = root.join("escaping-link"); + symlink_dir(outside.path(), &escaping); + + let removed = remove_escaping_symlinks(&root, &root).unwrap(); + + assert_eq!(removed, vec![escaping.clone()]); + assert!(is_symlink(&internal)); + assert!(!is_symlink(&escaping)); + assert!(outside.path().exists()); + assert_eq!(fs::read(root.join("sub/keep.txt")).unwrap(), b"data"); + } + + #[cfg(unix)] + #[test] + fn removes_dangling_escaping_symlinks() { + let tmp = tempfile::tempdir().unwrap(); + let root = tmp.path().join("game"); + fs::create_dir_all(&root).unwrap(); + + let dangling = root.join("dangling-link"); + std::os::unix::fs::symlink("../outside-missing/target", &dangling).unwrap(); + + let removed = remove_escaping_symlinks(&root, &root).unwrap(); + + assert_eq!(removed, vec![dangling.clone()]); + assert!(!is_symlink(&dangling)); + } + + #[cfg(unix)] + #[test] + fn removes_symlink_chains_that_escape() { + let tmp = tempfile::tempdir().unwrap(); + let root = tmp.path().join("game"); + let outside = tempfile::tempdir().unwrap(); + fs::create_dir_all(&root).unwrap(); + + let hop = root.join("hop"); + symlink_dir(outside.path(), &hop); + let chain = root.join("chain"); + std::os::unix::fs::symlink("hop", &chain).unwrap(); + + let removed = remove_escaping_symlinks(&root, &root).unwrap(); + + assert_eq!(removed.len(), 2); + assert!(!is_symlink(&hop)); + assert!(!is_symlink(&chain)); + } + + #[cfg(unix)] + #[test] + fn rejects_scan_directories_outside_the_root() { + let tmp = tempfile::tempdir().unwrap(); + let root = tmp.path().join("game"); + let outside = tmp.path().join("outside"); + fs::create_dir_all(&root).unwrap(); + fs::create_dir_all(&outside).unwrap(); + + assert!(remove_escaping_symlinks(&root, &outside).is_err()); + } } diff --git a/desktop/src-tauri/process/src/pipeline.rs b/desktop/src-tauri/process/src/pipeline.rs index 0e267e097..aa15efced 100644 --- a/desktop/src-tauri/process/src/pipeline.rs +++ b/desktop/src-tauri/process/src/pipeline.rs @@ -734,7 +734,9 @@ async fn execute_extract_rar( cancel_flag, parse_7z_progress, ) - .await + .await?; + + sweep_extraction_symlinks(install_dir, &output_dir) } async fn execute_extract_iso( @@ -833,7 +835,7 @@ async fn execute_extract_iso( .await?; } - Ok(()) + sweep_extraction_symlinks(install_dir, &output_dir) } async fn execute_extract_archive( @@ -885,7 +887,9 @@ async fn execute_extract_archive( cancel_flag, parse_7z_progress, ) - .await + .await?; + + sweep_extraction_symlinks(install_dir, &output_dir) } async fn execute_innoextract( @@ -934,7 +938,9 @@ async fn execute_innoextract( cancel_flag, parse_innoextract_progress, ) - .await + .await?; + + sweep_extraction_symlinks(install_dir, &output_dir) } async fn execute_apply_crack( @@ -1052,6 +1058,30 @@ pub fn overlay_directory(src_dir: &Path, dest_dir: &Path) -> Result<(), String> Ok(()) } +/// Reject symlinks an external extractor materialized that resolve outside the +/// install directory. Escaping links are unlinked before the error is returned +/// so no dangerous path survives a failed step. See +/// `docs/implementation/archive-symlink-policy.md`. +fn sweep_extraction_symlinks(install_dir: &Path, output_dir: &Path) -> Result<(), String> { + let escaped = crate::path_guard::remove_escaping_symlinks(install_dir, output_dir) + .map_err(|e| format!("Failed to sweep extracted symlinks: {e}"))?; + if escaped.is_empty() { + return Ok(()); + } + + let list = escaped + .iter() + .map(|path| path.display().to_string()) + .collect::>() + .join(", "); + warn!("Removed escaping symlinks after extraction: {list}"); + Err(format!( + "Extraction produced {} symlink(s) escaping the install directory; \ + removed them: {list}", + escaped.len() + )) +} + /// Resolve `candidate` inside `install_dir` and return it only when it is an /// existing directory. Absolute paths, `..` escapes and symlinked components /// are rejected. @@ -1659,6 +1689,44 @@ mod tests { assert!(!outside.join("evil.dll").exists()); } + #[test] + fn test_sweep_extraction_symlinks_accepts_clean_directory() { + let temp = tempfile::tempdir().unwrap(); + let install = temp.path().join("game"); + fs::create_dir_all(install.join("sub")).unwrap(); + fs::write(install.join("sub/game.exe"), b"x").unwrap(); + + assert!(sweep_extraction_symlinks(&install, &install).is_ok()); + } + + #[cfg(unix)] + #[test] + fn test_sweep_extraction_symlinks_rejects_and_removes_escape() { + let temp = tempfile::tempdir().unwrap(); + let install = temp.path().join("game"); + let outside = temp.path().join("outside"); + fs::create_dir_all(&install).unwrap(); + fs::create_dir_all(&outside).unwrap(); + let escape = install.join("escape"); + std::os::unix::fs::symlink(outside.join("victim"), &escape).unwrap(); + + assert!(sweep_extraction_symlinks(&install, &install).is_err()); + assert!(!crate::path_guard::is_symlink(&escape)); + } + + #[cfg(unix)] + #[test] + fn test_sweep_extraction_symlinks_keeps_internal_links() { + let temp = tempfile::tempdir().unwrap(); + let install = temp.path().join("game"); + fs::create_dir_all(install.join("sub")).unwrap(); + let internal = install.join("internal"); + std::os::unix::fs::symlink(install.join("sub"), &internal).unwrap(); + + assert!(sweep_extraction_symlinks(&install, &install).is_ok()); + assert!(crate::path_guard::is_symlink(&internal)); + } + #[test] fn test_materialize_setup_script() { let temp = tempfile::tempdir().unwrap(); diff --git a/docs/implementation/archive-symlink-policy.md b/docs/implementation/archive-symlink-policy.md new file mode 100644 index 000000000..46a49fda0 --- /dev/null +++ b/docs/implementation/archive-symlink-policy.md @@ -0,0 +1,62 @@ +# Archive extraction symlink policy + +## Threat + +Game recipes extract untrusted release archives with 7-Zip (`7z x -y -bsp1`) and +innoextract directly into the install directory. Both tools honor symlink entries +stored in an archive. `path_guard` only protects writes Drop performs itself +(`safe_join`, `write_file`, `copy_to`, `ensure_dir`) and `overlay_directory` +skips symlink entries, so a link planted by the extractor bypasses every guard. +A link such as `../../.ssh/authorized_keys` or an absolute path to a user +document can be followed by later pipeline steps, the game, or the user. + +## Policy + +After every successful external extraction step (`extract_rar`, `extract_iso`, +`extract_archive`, `innoextract`) the pipeline sweeps the extraction output +directory: + +- Symlinks whose resolved target stays **inside** the install directory are + kept. Linux releases legitimately ship relative links between game files. +- Symlinks whose resolved target **escapes** the install directory are unlinked + ("quarantined by removal"), logged, and the step fails with an actionable + error naming the removed links. The install is therefore never left with a + dangerous link even when the step is reported as failed. +- Dangling links are resolved lexically (`parent + target`, `.`/`..` collapsed) + so a link cannot escape merely because its target does not exist yet. +- Symlinked directories are never traversed by the sweep + (`follow_links(false)`), so a link cannot hide escaping links behind it. + +## Implementation + +- `desktop/src-tauri/process/src/path_guard.rs` + - `remove_escaping_symlinks(root, scan_dir)` canonicalizes the install + directory, refuses a scan directory outside it, walks it without following + links, and unlinks escaping links. Returns the removed paths. + - `symlink_escapes` uses `fs::canonicalize` for resolvable targets and falls + back to `lexical_normalize(parent.join(target))` for dangling ones. +- `desktop/src-tauri/process/src/pipeline.rs` + - `sweep_extraction_symlinks(install_dir, output_dir)` wraps the path guard + helper, logs removed links and turns a non-empty result into a failed step. + - Called after `extract_rar`, `extract_iso`, `extract_archive` and + `innoextract`. + +## Out of scope / back-compat + +- `path_guard` continues to reject symlinked components for every path Drop + writes itself; the sweep is specifically the post-extractor safety net. +- `run_command` steps are recipe-authored and their program is resolved inside + the install directory by `resolve_command_program`; symlinks they create + themselves are not swept. +- The crack overlay copies regular files only (`overlay_directory` skips + symlinks), so it never propagates an escaping link. +- On Windows, creating symlinks normally requires elevation, so archives + usually materialize links as regular files there; the sweep is still run and + is a no-op when no links exist. + +## Tests + +`desktop/src-tauri/process/src/path_guard.rs` covers escaping links, dangling +escaping links, escaping link chains, internal links being kept, and scan +directories outside the root. `pipeline.rs` covers the step wrapper failing and +removing an escape, keeping internal links, and accepting clean directories. From e7c1786e1ffdca585ee2f7439a9ab7defea619cc Mon Sep 17 00:00:00 2001 From: John Smith Date: Mon, 14 Sep 2026 09:10:31 -0400 Subject: [PATCH 195/243] chore(security): run cargo-deny over every Rust crate --- .github/workflows/security.yml | 20 ++++++-- cli/Cargo.toml | 1 + deny.toml | 15 +++++- desktop/src-tauri/Cargo.lock | 4 +- desktop/src-tauri/Cargo.toml | 1 + desktop/src-tauri/client/Cargo.toml | 1 + desktop/src-tauri/cloud_saves/Cargo.toml | 1 + desktop/src-tauri/database/Cargo.toml | 1 + desktop/src-tauri/download_manager/Cargo.toml | 1 + desktop/src-tauri/games/Cargo.toml | 1 + desktop/src-tauri/input/Cargo.toml | 1 + desktop/src-tauri/process/Cargo.toml | 1 + desktop/src-tauri/remote/Cargo.toml | 1 + desktop/src-tauri/shader_cache/Cargo.toml | 1 + desktop/src-tauri/utils/Cargo.toml | 1 + .../rust-supply-chain-policy.md | 46 +++++++++++++++++++ 16 files changed, 90 insertions(+), 7 deletions(-) create mode 100644 docs/implementation/rust-supply-chain-policy.md diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index f4f8e4b5d..90324c8ca 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -64,6 +64,17 @@ jobs: cargo-deny: name: Rust dependency policy runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + crate: + - cli + - torrential + - desktop/src-tauri + - libraries/droplet + - libraries/droplet_types + - libraries/libarchive + - libraries/native_model steps: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # actions/checkout@v4 with: @@ -72,13 +83,14 @@ jobs: - uses: taiki-e/install-action@4b8992af23a2be34fd58f8d9b50d3f4939ad1891 # taiki-e/install-action@cargo-audit with: tool: cargo-deny - - name: Check advisories, licenses, bans and sources - # Uses the repository-level deny.toml. Extend the working directory (or - # matrix) once other crates have been triaged. + - name: Check advisories, licenses, bans and sources (${{ matrix.crate }}) + # cargo-deny discovers the repository-level deny.toml by walking up from + # the crate directory. Vendored crates gitignore their Cargo.lock; + # generate one on the fly so the dependency graph can be resolved. run: | if [ ! -f Cargo.lock ]; then cargo generate-lockfile; fi cargo deny check - working-directory: torrential + working-directory: ${{ matrix.crate }} golangci: name: Go lint (backend) diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 661ca5a27..9fd7a0ca8 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -2,6 +2,7 @@ name = "downpour" version = "0.1.0" edition = "2024" +license = "AGPL-3.0-only" [dependencies] anyhow = "1.0.100" diff --git a/deny.toml b/deny.toml index abc4dd96f..b2fba4d7e 100644 --- a/deny.toml +++ b/deny.toml @@ -6,9 +6,22 @@ all-features = true [advisories] version = 2 +# Only flag unmaintained crates that a workspace directly depends on. The +# transitive unmaintained stack (async-std, instant, unic-*, proc-macro-error, +# GTK-era crates, ...) comes from Tauri/Linux bindings and can only be retired +# upstream; `cargo audit` still reports it weekly in security.yml. Any +# first-party dependency that goes unmaintained must be replaced rather than +# ignored. +unmaintained = "workspace" # Fail on known vulnerabilities. Add scoped ignores here with a comment and a # tracking issue if a transitive advisory cannot be resolved immediately. -ignore = [] +ignore = [ + # Unmaintained (not vulnerable) advisory for bincode 1.3.3/2.0.1, a direct + # dependency of the vendored `native_model` crate for its versioned + # serialization ABI. No safe upgrade exists and swapping the format would + # break stored/consumed models; revisit when `native_model` migrates. + { id = "RUSTSEC-2025-0141", reason = "bincode is a direct dependency of the vendored native_model crate" }, +] [licenses] version = 2 diff --git a/desktop/src-tauri/Cargo.lock b/desktop/src-tauri/Cargo.lock index afeff7fc0..3bb76a7ff 100644 --- a/desktop/src-tauri/Cargo.lock +++ b/desktop/src-tauri/Cargo.lock @@ -693,9 +693,9 @@ checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" [[package]] name = "chacha20" -version = "0.10.1" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" +checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06" dependencies = [ "cfg-if", "cpufeatures 0.3.0", diff --git a/desktop/src-tauri/Cargo.toml b/desktop/src-tauri/Cargo.toml index 4692ea76a..fbf7be491 100644 --- a/desktop/src-tauri/Cargo.toml +++ b/desktop/src-tauri/Cargo.toml @@ -4,6 +4,7 @@ version = "0.4.0" description = "The client application for the open-source, self-hosted game distribution platform Drop" authors = ["Drop OSS"] edition = "2024" +license = "AGPL-3.0-only" [workspace] resolver = "3" diff --git a/desktop/src-tauri/client/Cargo.toml b/desktop/src-tauri/client/Cargo.toml index 03dd6c3b6..1778b4ef9 100644 --- a/desktop/src-tauri/client/Cargo.toml +++ b/desktop/src-tauri/client/Cargo.toml @@ -2,6 +2,7 @@ name = "client" version = "0.1.0" edition = "2024" +license = "AGPL-3.0-only" [dependencies] bitcode = "*" diff --git a/desktop/src-tauri/cloud_saves/Cargo.toml b/desktop/src-tauri/cloud_saves/Cargo.toml index ae0864434..f36c598e1 100644 --- a/desktop/src-tauri/cloud_saves/Cargo.toml +++ b/desktop/src-tauri/cloud_saves/Cargo.toml @@ -2,6 +2,7 @@ name = "cloud_saves" version = "0.1.0" edition = "2024" +license = "AGPL-3.0-only" [dependencies] chrono = "0.4.42" diff --git a/desktop/src-tauri/database/Cargo.toml b/desktop/src-tauri/database/Cargo.toml index 0061a7103..835751470 100644 --- a/desktop/src-tauri/database/Cargo.toml +++ b/desktop/src-tauri/database/Cargo.toml @@ -2,6 +2,7 @@ name = "database" version = "0.1.0" edition = "2024" +license = "AGPL-3.0-only" [dependencies] aes = "0.8.4" diff --git a/desktop/src-tauri/download_manager/Cargo.toml b/desktop/src-tauri/download_manager/Cargo.toml index 1804c8218..341df89a2 100644 --- a/desktop/src-tauri/download_manager/Cargo.toml +++ b/desktop/src-tauri/download_manager/Cargo.toml @@ -2,6 +2,7 @@ name = "download_manager" version = "0.1.0" edition = "2024" +license = "AGPL-3.0-only" [dependencies] async-trait = "0.1.89" diff --git a/desktop/src-tauri/games/Cargo.toml b/desktop/src-tauri/games/Cargo.toml index 0d7a68d6c..8fc798313 100644 --- a/desktop/src-tauri/games/Cargo.toml +++ b/desktop/src-tauri/games/Cargo.toml @@ -2,6 +2,7 @@ name = "games" version = "0.1.0" edition = "2024" +license = "AGPL-3.0-only" [dependencies] aes = "0.8.4" diff --git a/desktop/src-tauri/input/Cargo.toml b/desktop/src-tauri/input/Cargo.toml index e462337f9..911463030 100644 --- a/desktop/src-tauri/input/Cargo.toml +++ b/desktop/src-tauri/input/Cargo.toml @@ -2,6 +2,7 @@ name = "input" version = "0.1.0" edition = "2024" +license = "AGPL-3.0-only" [dependencies] diff --git a/desktop/src-tauri/process/Cargo.toml b/desktop/src-tauri/process/Cargo.toml index 4c7892c45..ef55f3d2f 100644 --- a/desktop/src-tauri/process/Cargo.toml +++ b/desktop/src-tauri/process/Cargo.toml @@ -2,6 +2,7 @@ name = "process" version = "0.1.0" edition = "2024" +license = "AGPL-3.0-only" [dependencies] chrono = "0.4.42" diff --git a/desktop/src-tauri/remote/Cargo.toml b/desktop/src-tauri/remote/Cargo.toml index c2198dbcd..254d12848 100644 --- a/desktop/src-tauri/remote/Cargo.toml +++ b/desktop/src-tauri/remote/Cargo.toml @@ -2,6 +2,7 @@ name = "remote" version = "0.1.0" edition = "2024" +license = "AGPL-3.0-only" [dependencies] async-trait = "0.1.89" diff --git a/desktop/src-tauri/shader_cache/Cargo.toml b/desktop/src-tauri/shader_cache/Cargo.toml index 53f3c173b..0dca756e5 100644 --- a/desktop/src-tauri/shader_cache/Cargo.toml +++ b/desktop/src-tauri/shader_cache/Cargo.toml @@ -2,6 +2,7 @@ name = "shader_cache" version = "0.1.0" edition = "2024" +license = "AGPL-3.0-only" [dependencies] walkdir = "2.5" diff --git a/desktop/src-tauri/utils/Cargo.toml b/desktop/src-tauri/utils/Cargo.toml index 921e8521a..103f08421 100644 --- a/desktop/src-tauri/utils/Cargo.toml +++ b/desktop/src-tauri/utils/Cargo.toml @@ -2,6 +2,7 @@ name = "utils" version = "0.1.0" edition = "2024" +license = "AGPL-3.0-only" [dependencies] log = "0.4.28" diff --git a/docs/implementation/rust-supply-chain-policy.md b/docs/implementation/rust-supply-chain-policy.md new file mode 100644 index 000000000..ddf774864 --- /dev/null +++ b/docs/implementation/rust-supply-chain-policy.md @@ -0,0 +1,46 @@ +# Rust supply-chain policy (cargo-deny) + +`cargo-deny` runs from the repository root `deny.toml` for every Rust crate in +the repo. The `cargo-deny` job in `.github/workflows/security.yml` uses the same +matrix as `cargo-audit`: + +`cli`, `torrential`, `desktop/src-tauri`, `libraries/droplet`, +`libraries/droplet_types`, `libraries/libarchive`, `libraries/native_model`. + +`cargo-deny` walks up from the crate directory to find the root `deny.toml`. +Vendored crates that gitignore their `Cargo.lock` (`libraries/libarchive`, +`libraries/native_model`) get a generated lockfile in CI, matching how +`cargo-audit` already runs. + +## Policy + +- **advisories**: vulnerabilities fail; unmaintained advisories are scoped with + `unmaintained = "workspace"`. Only unmaintained crates the workspace depends + on directly fail the gate. The transitive unmaintained stack pulled in by + Tauri/GTK/async runtime crates (`async-std`, `instant`, `proc-macro-error`, + `unic-*`, …) is still reported by the weekly `cargo-audit` job and can only be + retired upstream. +- **licenses**: permissive licenses and AGPL variants are allowed. First-party + crates must declare a license; all workspace manifests now declare + `AGPL-3.0-only` (matching `torrential`, `droplet`, and `droplet_types`). +- **bans / sources**: warnings for duplicate versions (report-only), no + wildcard bans, unknown registries and git sources denied. + +## Scoped ignore + +- `RUSTSEC-2025-0141` (`bincode` unmaintained): `bincode` 1.3.3/2.0.1 is a + direct dependency of the vendored `native_model` crate for its versioned + serialization ABI. The advisory is unmaintained-only (no known + vulnerability), no safe upgrade exists, and changing the format would break + stored models. Revisit when `native_model` migrates. + `cargo-deny` reports `advisory-not-detected` warnings for this ignore in + crates whose graphs do not contain `bincode`; the warning is informational. + +## Left as-is (documented) + +- Duplicate-version warnings from `[bans] multiple-versions = "warn"` remain + report-only. +- `license-not-encountered` warnings note allow-list entries that a given crate + graph does not use; this is expected with a shared root policy. +- The yanked `chacha20 0.10.1` entry in `desktop/src-tauri/Cargo.lock` was + refreshed to `0.10.2` (`cargo update -p chacha20`) as part of this change. From 41a7eab25535f4986965c443538d474b2716dd4d Mon Sep 17 00:00:00 2001 From: John Smith Date: Mon, 14 Sep 2026 09:10:36 -0400 Subject: [PATCH 196/243] fix(server): clear the session cookie when a session expires --- server/server/internal/session/index.ts | 29 ++++++++++++++++++++----- 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/server/server/internal/session/index.ts b/server/server/internal/session/index.ts index 17a43aa8b..e0ece701b 100644 --- a/server/server/internal/session/index.ts +++ b/server/server/internal/session/index.ts @@ -1,4 +1,5 @@ import type { H3Event } from "h3"; +import { isEvent } from "h3"; import type { OIDCData, Session, @@ -130,7 +131,9 @@ export class SessionHandler { // if expired session if (new Date(session.expiresAt).getTime() < Date.now()) { await this.sessionProvider.removeSession(token); - // TODO: should probably call signout to clear the cookie + // Expired sessions must not leave a stale cookie behind. Only a real + // H3Event can write the response; headers-only callers cannot. + if (isEvent(request)) this.clearSessionCookie(request); // session expired return undefined; } @@ -188,7 +191,7 @@ export class SessionHandler { const token = this.getSessionToken(h3); if (!token) return false; if (!(await this.signoutByToken(token))) return false; - deleteCookie(h3, dropTokenCookieName, { path: "/" }); + this.clearSessionCookie(h3); return true; } @@ -257,6 +260,23 @@ export class SessionHandler { .toJSDate(); } + /** + * Attributes shared by setting and clearing the session cookie, so a cleared + * cookie always matches the one that was set. + */ + private sessionCookieOptions() { + return { + path: "/", + httpOnly: true, + sameSite: "lax" as const, + secure: (process.env.EXTERNAL_URL ?? "").startsWith("https://"), + }; + } + + private clearSessionCookie(h3: H3Event) { + deleteCookie(h3, dropTokenCookieName, this.sessionCookieOptions()); + } + /** * Creates cookie that represents user session * @param h3 @@ -268,11 +288,8 @@ export class SessionHandler { // TODO: we should probably switch to jwts to minimize possibility of someone // trying to guess a session id (jwts let us sign + encrypt stuff in a std way) setCookie(h3, dropTokenCookieName, token, { + ...this.sessionCookieOptions(), expires: expiresAt, - path: "/", - httpOnly: true, - sameSite: "lax", - secure: (process.env.EXTERNAL_URL ?? "").startsWith("https://"), }); return token; } From 4264a59a6439388ceb7bc5800764bb96ad8e6661 Mon Sep 17 00:00:00 2001 From: John Smith Date: Mon, 14 Sep 2026 09:10:41 -0400 Subject: [PATCH 197/243] fix(server): require explicit opt-in to overwrite objects --- .../server/api/v1/object/[id]/index.post.ts | 3 + .../objects/__tests__/objectHandler.test.ts | 163 ++++++++++++++++++ server/server/internal/objects/fsBackend.ts | 4 + .../server/internal/objects/objectHandler.ts | 10 +- 4 files changed, 179 insertions(+), 1 deletion(-) create mode 100644 server/server/internal/objects/__tests__/objectHandler.test.ts diff --git a/server/server/api/v1/object/[id]/index.post.ts b/server/server/api/v1/object/[id]/index.post.ts index e6779fff4..de3781461 100644 --- a/server/server/api/v1/object/[id]/index.post.ts +++ b/server/server/api/v1/object/[id]/index.post.ts @@ -18,10 +18,13 @@ export default defineEventHandler(async (h3) => { const buffer = Buffer.from(body); const id = sanitize(unsafeId); + // This endpoint is the explicit object-update path (`object:update`), so it + // opts into overwriting; every other upload path creates a fresh object id. const result = await objectHandler.writeWithPermissions( id, async () => buffer, userId, + { allowOverwrite: true }, ); return { success: result }; }); diff --git a/server/server/internal/objects/__tests__/objectHandler.test.ts b/server/server/internal/objects/__tests__/objectHandler.test.ts new file mode 100644 index 000000000..c509d259f --- /dev/null +++ b/server/server/internal/objects/__tests__/objectHandler.test.ts @@ -0,0 +1,163 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { Readable, type Writable } from "node:stream"; +import type pino from "pino"; +import { + ObjectBackend, + ObjectHandler, + type ObjectMetadata, + type Source, +} from "../objectHandler"; + +const OWNER = "user-1"; + +class MemoryBackend extends ObjectBackend { + private readonly objects = new Map(); + private readonly metadata = new Map(); + + seed(id: string, content: Buffer, permissions: string[]) { + this.objects.set(id, content); + this.metadata.set(id, { + mime: "application/octet-stream", + permissions, + userMetadata: {}, + }); + } + + content(id: string) { + return this.objects.get(id); + } + + async fetch(id: string): Promise { + const content = this.objects.get(id); + return content ? Readable.from(content) : undefined; + } + + async write(id: string, source: Source): Promise { + if (!this.objects.has(id)) return false; + if (source instanceof Readable) { + const chunks: Buffer[] = []; + for await (const chunk of source) + chunks.push(Buffer.from(chunk as Buffer)); + this.objects.set(id, Buffer.concat(chunks)); + } else { + this.objects.set(id, source); + } + return true; + } + + async startWriteStream(_id: string): Promise { + return undefined; + } + + async create( + id: string, + source: Source, + metadata: ObjectMetadata, + ): Promise { + this.seed(id, Buffer.from(""), metadata.permissions); + await this.write(id, source); + return id; + } + + async createWithWriteStream( + id: string, + metadata: ObjectMetadata, + ): Promise { + this.seed(id, Buffer.from(""), metadata.permissions); + return undefined; + } + + async delete(id: string) { + this.objects.delete(id); + this.metadata.delete(id); + return true; + } + + async fetchMetadata(id: string) { + return this.metadata.get(id); + } + + async writeMetadata(id: string, metadata: ObjectMetadata) { + this.metadata.set(id, metadata); + return true; + } + + async fetchHash() { + return undefined; + } + + async listAll() { + return [...this.objects.keys()]; + } + + async cleanupMetadata(_taskLogger: pino.Logger) { + return; + } +} + +test("writeWithPermissions refuses to overwrite by default", async () => { + const backend = new MemoryBackend(); + backend.seed("obj", Buffer.from("original"), [`${OWNER}:write`]); + const handler = new ObjectHandler(backend); + + let fetched = false; + const result = await handler.writeWithPermissions( + "obj", + async () => { + fetched = true; + return Buffer.from("pwned"); + }, + OWNER, + ); + + assert.equal(result, false); + assert.equal(fetched, false); + assert.equal(backend.content("obj")?.toString(), "original"); +}); + +test("writeWithPermissions overwrites when the caller opts in with write permission", async () => { + const backend = new MemoryBackend(); + backend.seed("obj", Buffer.from("original"), [`${OWNER}:write`]); + const handler = new ObjectHandler(backend); + + const result = await handler.writeWithPermissions( + "obj", + async () => Buffer.from("updated"), + OWNER, + { allowOverwrite: true }, + ); + + assert.equal(result, true); + assert.equal(backend.content("obj")?.toString(), "updated"); +}); + +test("writeWithPermissions still requires write permission when opted in", async () => { + const backend = new MemoryBackend(); + backend.seed("obj", Buffer.from("original"), [`${OWNER}:read`]); + const handler = new ObjectHandler(backend); + + const result = await handler.writeWithPermissions( + "obj", + async () => Buffer.from("pwned"), + OWNER, + { allowOverwrite: true }, + ); + + assert.equal(result, false); + assert.equal(backend.content("obj")?.toString(), "original"); +}); + +test("writeWithPermissions refuses unknown objects", async () => { + const backend = new MemoryBackend(); + const handler = new ObjectHandler(backend); + + const result = await handler.writeWithPermissions( + "missing", + async () => Buffer.from("pwned"), + OWNER, + { allowOverwrite: true }, + ); + + assert.equal(result, false); +}); diff --git a/server/server/internal/objects/fsBackend.ts b/server/server/internal/objects/fsBackend.ts index 8fc493dc3..7e0061d0b 100644 --- a/server/server/internal/objects/fsBackend.ts +++ b/server/server/internal/objects/fsBackend.ts @@ -68,6 +68,10 @@ export class FsObjectBackend extends ObjectBackend { await this.hashStore.delete(id); try { + // Replace, don't patch: a shorter payload must not leave trailing bytes + // of the previous object behind. + await handle.truncate(0); + if (source instanceof Readable) { const outputStream = handle.createWriteStream({ autoClose: true }); await Stream.promises.pipeline(source, outputStream); diff --git a/server/server/internal/objects/objectHandler.ts b/server/server/internal/objects/objectHandler.ts index e9f1635ea..758c3cf66 100644 --- a/server/server/internal/objects/objectHandler.ts +++ b/server/server/internal/objects/objectHandler.ts @@ -219,17 +219,24 @@ export class ObjectHandler { * @param id object id * @param sourceFetcher callback used to provide image * @param userId user to check, or act as anon user + * @param options overwrite must be opted into explicitly * @returns * @description If we need to fetch a remote resource, it doesn't make sense * to immediately fetch the object, *then* check permissions. * Instead the caller can pass a simple anonymous function, like * () => $dropFetch('/my-image'); * And if we actually have permission to write, it fetches it then. + * + * Objects are create-once by default: an existing object is never clobbered + * unless the caller opts in. The only caller that does is the generic + * `POST /api/v1/object/[id]` update endpoint, whose ACL explicitly grants + * `object:update`; every other upload path creates a fresh object id. */ async writeWithPermissions( id: string, sourceFetcher: () => Promise, userId?: string, + options?: { allowOverwrite?: boolean }, ) { const metadata = await this.backend.fetchMetadata(id); if (!metadata) return false; @@ -241,8 +248,9 @@ export class ObjectHandler { if (!hasPermission) return false; + if (!options?.allowOverwrite) return false; + const source = await sourceFetcher(); - // TODO: prevent user from overwriting existing object const result = await this.backend.write(id, source); return result; From 1b6df12a9eefe473551952fdeaa2bc879ba8746f Mon Sep 17 00:00:00 2001 From: John Smith Date: Mon, 14 Sep 2026 09:10:46 -0400 Subject: [PATCH 198/243] fix(server): make update checks ETag-aware and rate-limit tolerant --- .../server/internal/tasks/registry/update.ts | 113 ++++++++++++++---- 1 file changed, 92 insertions(+), 21 deletions(-) diff --git a/server/server/internal/tasks/registry/update.ts b/server/server/internal/tasks/registry/update.ts index 994e56472..6f764aa36 100644 --- a/server/server/internal/tasks/registry/update.ts +++ b/server/server/internal/tasks/registry/update.ts @@ -1,6 +1,8 @@ import { type } from "arktype"; +import type pino from "pino"; import * as semver from "semver"; import { defineDropTask } from ".."; +import cacheHandler from "../../cache"; import { systemConfig } from "../../config/sys-conf"; import notificationSystem from "../../notifications"; @@ -16,15 +18,91 @@ const latestRelease = type({ published_at: "string", }); +const UPDATE_CHECK_URL = + "https://api.github.com/repos/Drop-OSS/drop/releases/latest"; +const UPDATE_CHECK_CACHE_KEY = "latest-release"; +type CachedRelease = { etag: string; release: Record }; +const updateCheckCache = cacheHandler.createCache("UpdateCheck"); + +function isRateLimited(response: Response) { + if (response.status === 429) return true; + if (response.status !== 403) return false; + return response.headers.get("x-ratelimit-remaining") === "0"; +} + +function githubHeaders(): Record { + const headers: Record = { + Accept: "application/vnd.github+json", + "X-GitHub-Api-Version": "2022-11-28", + }; + // Authenticated requests get a much higher rate limit; the token is optional + // so self-hosted instances without one still work. + const token = process.env.GITHUB_TOKEN; + if (token) headers.Authorization = `Bearer ${token}`; + return headers; +} + +type FetchReleaseResult = + | { status: "release"; release: unknown } + | { status: "rate-limited"; resetAt: string }; + +async function fetchLatestRelease( + logger: pino.Logger, +): Promise { + const headers = githubHeaders(); + const cached = await updateCheckCache.get(UPDATE_CHECK_CACHE_KEY); + if (cached?.etag) headers["If-None-Match"] = cached.etag; + + const response = await fetch(UPDATE_CHECK_URL, { headers }); + + if (response.status === 304) { + // Conditional request: the release has not changed since the last check. + if (!cached?.release) { + throw new Error( + "GitHub returned 304 Not Modified but no cached release is available", + ); + } + return { status: "release", release: cached.release }; + } + + if (response.ok) { + const release = await response.json(); + const etag = response.headers.get("etag"); + if (etag) { + await updateCheckCache.set(UPDATE_CHECK_CACHE_KEY, { + etag, + release: release as CachedRelease["release"], + }); + } + return { status: "release", release }; + } + + if (isRateLimited(response)) { + // A crashloop or shared egress IP can exhaust the anonymous quota; this + // is transient, so callers log an actionable warning instead of failing. + const reset = response.headers.get("x-ratelimit-reset"); + const resetAt = reset + ? new Date(Number.parseInt(reset, 10) * 1000).toISOString() + : "unknown"; + return { status: "rate-limited", resetAt }; + } + + const errorBody = await response.text().catch(() => ""); + logger.info( + { status: response.status, body: errorBody }, + "Failed to check for update ", + ); + throw new Error( + `Failed to check for update: ${response.status} ${errorBody}`, + ); +} + export default defineDropTask({ buildId: () => `check:update:${new Date().toISOString()}`, name: "Check for Update", acls: ["system:maintenance:read"], taskGroup: "check:update", async run({ progress, logger }) { - // TODO: maybe implement some sort of rate limit thing to prevent this from calling github api a bunch in the event of crashloop or whatever? - // probably will require custom task scheduler for object cleanup anyway, so something to thing about - if (!systemConfig.shouldCheckForUpdates()) { logger.info("Update check is disabled by configuration"); progress(100); @@ -42,32 +120,25 @@ export default defineDropTask({ } progress(30); - const response = await fetch( - "https://api.github.com/repos/Drop-OSS/drop/releases/latest", - ); + const result = await fetchLatestRelease(logger); progress(50); - // if response failed somehow - if (!response.ok) { - logger.info( - { - status: response.status, - body: response.body, - }, - "Failed to check for update ", - ); - - throw new Error( - `Failed to check for update: ${response.status} ${JSON.stringify(response.body)}`, + if (result.status === "rate-limited") { + logger.warn( + { resetAt: result.resetAt }, + "GitHub API rate limit hit; set GITHUB_TOKEN to raise the limit", ); + progress(100); + return; } + const releaseJson = result.release; + // parse and validate response - const resJson = await response.json(); - const body = latestRelease(resJson); + const body = latestRelease(releaseJson); if (body instanceof type.errors) { logger.info(body.summary); - logger.info("GitHub Api response" + JSON.stringify(resJson)); + logger.info("GitHub Api response" + JSON.stringify(releaseJson)); throw new Error( `GitHub Api response did not match expected schema: ${body.summary}`, ); From b72dc0fb1a31de1a845adcaaf4cd3ba58e26ea6c Mon Sep 17 00:00:00 2001 From: John Smith Date: Mon, 14 Sep 2026 09:10:52 -0400 Subject: [PATCH 199/243] fix(cloud_saves): skip unreadable save paths instead of panicking --- desktop/src-tauri/cloud_saves/src/resolver.rs | 44 +++++++++++++++---- 1 file changed, 35 insertions(+), 9 deletions(-) diff --git a/desktop/src-tauri/cloud_saves/src/resolver.rs b/desktop/src-tauri/cloud_saves/src/resolver.rs index efa9296f9..ab4c8916a 100644 --- a/desktop/src-tauri/cloud_saves/src/resolver.rs +++ b/desktop/src-tauri/cloud_saves/src/resolver.rs @@ -1,5 +1,5 @@ use std::{ - fs::{self, create_dir_all, File}, + fs::{self, File, create_dir_all}, io::{self, Read, Write}, path::{Path, PathBuf}, }; @@ -44,14 +44,40 @@ pub fn resolve(meta: &mut CloudSaveMetadata) -> File { None => continue, }; let t_path = PathBuf::from(normalize(&file.path, os)); - let path = parse_path(t_path, handler, &meta.game_version).unwrap(); - let f = std::fs::metadata(&path).unwrap(); // TODO: Fix unwrap here - if f.is_dir() { - tarball.append_dir_all(&id, path).unwrap(); - } else if f.is_file() { - tarball - .append_file(&id, &mut File::open(path).unwrap()) - .unwrap(); + let path = match parse_path(t_path, handler, &meta.game_version) { + Ok(path) => path, + Err(err) => { + warn!("Skipping save path for {:?}: {err}", &file); + continue; + } + }; + let metadata = match std::fs::metadata(&path) { + Ok(metadata) => metadata, + Err(err) => { + warn!("Skipping save path {}: {err}", path.display()); + continue; + } + }; + if metadata.is_dir() { + if let Err(err) = tarball.append_dir_all(&id, &path) { + warn!("Failed to archive directory {}: {err}", path.display()); + continue; + } + } else if metadata.is_file() { + let mut source = match File::open(&path) { + Ok(file) => file, + Err(err) => { + warn!( + "Skipping {} because it could not be opened: {err}", + path.display() + ); + continue; + } + }; + if let Err(err) = tarball.append_file(&id, &mut source) { + warn!("Failed to archive file {}: {err}", path.display()); + continue; + } } file.id = Some(id); } From 3b99bc5a501ce7196649270f3941dfcc26855a38 Mon Sep 17 00:00:00 2001 From: John Smith Date: Mon, 14 Sep 2026 09:10:55 -0400 Subject: [PATCH 200/243] fix(input): report only implemented virtual-gamepad backends --- desktop/src-tauri/input/src/lib.rs | 54 ++++++++++++------------------ 1 file changed, 22 insertions(+), 32 deletions(-) diff --git a/desktop/src-tauri/input/src/lib.rs b/desktop/src-tauri/input/src/lib.rs index fd0ef3f0a..16c537738 100644 --- a/desktop/src-tauri/input/src/lib.rs +++ b/desktop/src-tauri/input/src/lib.rs @@ -1,9 +1,10 @@ //! Drop Input — controller remapping and virtual gamepad abstraction (#18). //! -//! The platform backends live behind [`InputBackend`]: `uinput`/`evdev` on -//! Linux and `ViGEm` on Windows create the virtual device, while this crate -//! owns the pure mapping logic (deadzones, gyro-to-mouse, button remaps) so it -//! stays testable without touching `/dev/uinput` or a driver. +//! This crate owns the pure mapping logic (deadzones, gyro-to-mouse, button +//! remaps) so it stays testable without touching `/dev/uinput` or a driver. +//! No native virtual-device backend exists yet: [`InputBackend`] is implemented +//! only by [`MockBackend`] (tests/dry-runs) and [`NullBackend`] (explicit +//! unavailability), and [`supported_backends`] therefore reports only `Null`. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum GamepadButton { @@ -71,8 +72,9 @@ impl Default for GamepadState { } } -/// A virtual gamepad device. Real implementations wrap uinput (Linux) or ViGEm -/// (Windows); [`MockBackend`] records states for tests. +/// A virtual gamepad device. A future native implementation will wrap uinput +/// (Linux) or ViGEm (Windows); today [`MockBackend`] records states for tests +/// and [`NullBackend`] reports unavailability. pub trait InputBackend: Send + Sync { type Error; @@ -111,11 +113,7 @@ pub fn map_gyro_to_mouse(gyro: &Gyro, sensitivity: f32, deadzone: f32) -> MouseD } fn deadzone_degrees(value: f32, deadzone: f32) -> f32 { - if value.abs() <= deadzone { - 0.0 - } else { - value - } + if value.abs() <= deadzone { 0.0 } else { value } } /// Applies a deadzone to both sticks of a state. @@ -160,31 +158,26 @@ impl InputBackend for MockBackend { } /// Virtual gamepad backends Drop can target. +/// +/// `Uinput` and `ViGEm` are reserved API surface: no `InputBackend` +/// implementation exists for them yet, so advertising them would make callers +/// believe a device can be created. They stay in the enum so native backends +/// can be added later without breaking the public API. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum BackendKind { /// Linux `uinput`/`evdev` virtual device (requires `/dev/uinput` access). + /// Not implemented yet. Uinput, - /// Windows ViGEm bus driver. + /// Windows ViGEm bus driver. Not implemented yet. ViGEm, /// Always-present fallback that reports unavailability. Null, } -/// Backends available on the current platform, best first. `Null` is always -/// last so callers can surface a precise "no virtual gamepad available" error. -#[cfg(target_os = "linux")] -#[must_use] -pub fn supported_backends() -> &'static [BackendKind] { - &[BackendKind::Uinput, BackendKind::Null] -} - -#[cfg(target_os = "windows")] -#[must_use] -pub fn supported_backends() -> &'static [BackendKind] { - &[BackendKind::ViGEm, BackendKind::Null] -} - -#[cfg(not(any(target_os = "linux", target_os = "windows")))] +/// Backends available on the current platform. Only [`BackendKind::Null`] is +/// reported because no native `InputBackend` exists yet; the fallback surfaces +/// a precise `BackendUnavailable` error instead of pretending a virtual device +/// is available. #[must_use] pub fn supported_backends() -> &'static [BackendKind] { &[BackendKind::Null] @@ -294,10 +287,7 @@ mod tests { } #[test] - fn supported_backends_always_includes_the_null_fallback() { - let backends = supported_backends(); - assert!(backends.contains(&BackendKind::Null)); - #[cfg(target_os = "linux")] - assert!(backends.contains(&BackendKind::Uinput)); + fn supported_backends_reports_only_implemented_backends() { + assert_eq!(supported_backends(), &[BackendKind::Null]); } } From ac9851fa349665a89e3ae98f2159ef00e5156070 Mon Sep 17 00:00:00 2001 From: John Smith Date: Mon, 14 Sep 2026 09:10:59 -0400 Subject: [PATCH 201/243] fix(i18n): drop TODO markers from user activity strings --- server/i18n/locales/de.json | 2 +- server/i18n/locales/en_us.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/server/i18n/locales/de.json b/server/i18n/locales/de.json index be09af3f8..106e5e4eb 100644 --- a/server/i18n/locales/de.json +++ b/server/i18n/locales/de.json @@ -661,7 +661,7 @@ "editProfile": "Profil bearbeiten", "noActivity": "Keine aktuellen Ereignisse", "notFound": "Nutzer nicht gefunden", - "recent": "Kürzliche Aktivitäten (TODO)", + "recent": "Kürzliche Aktivitäten", "recentSub": "Kürzliche Aktivitäten von diesem Nutzer", "unknown": "Unbekannter Nutzer" }, diff --git a/server/i18n/locales/en_us.json b/server/i18n/locales/en_us.json index e0ec03c51..4bc1e11fd 100644 --- a/server/i18n/locales/en_us.json +++ b/server/i18n/locales/en_us.json @@ -785,7 +785,7 @@ "editProfile": "Edit profile", "noActivity": "No recent activity", "notFound": "User not found", - "recent": "Recent activity (TODO)", + "recent": "Recent activity", "recentSub": "Recent activity by this user", "unknown": "Unknown user" }, From 08d7f353b87dff765711b8768e76278e5984463d Mon Sep 17 00:00:00 2001 From: John Smith Date: Mon, 14 Sep 2026 09:11:03 -0400 Subject: [PATCH 202/243] docs: correct upstream PR refs and roadmap claims --- ROADMAP.md | 2 +- docs/implementation/roadmap-status.md | 40 ++++++++++--------- .../upstream-pr1-platform-stability.md | 8 +++- .../upstream-pr2-generic-plugin-spi.md | 8 +++- 4 files changed, 37 insertions(+), 21 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 6c65c682b..4f06caab0 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -29,7 +29,7 @@ Steam dominates PC gaming not just because it is a store, but because it is an i - **Local LAN P2P Chunk Sharing**: - Automatic peer discovery via mDNS on local subnets. - - P2P chunk transfer over HTTP/QUIC at multi-gigabit LAN speeds, reducing WAN bandwidth during LAN parties and multi-device homes. + - P2P chunk transfer over HTTP at multi-gigabit LAN speeds, reducing WAN bandwidth during LAN parties and multi-device homes. (QUIC is a deferred/optional transport: only the `ChunkFetcher` contract exists today, and HTTP is the implemented transport.) - **`downpour push` Developer Tooling**: - Headless CLI for game developers to upload builds to target depots and manage release branches (`main`, `beta`, `nightly`). - Rolling hash chunking, Zstandard compression, and binary deltas. diff --git a/docs/implementation/roadmap-status.md b/docs/implementation/roadmap-status.md index ba0ecc642..be196ce80 100644 --- a/docs/implementation/roadmap-status.md +++ b/docs/implementation/roadmap-status.md @@ -6,16 +6,16 @@ tests and a pushed commit; `🟡` usable contract/reference implementation; ## `` — Issue classification & core SPIs -| Area | Status | Evidence | -| :----------------------------------------------------------------------------------------------- | :----- | :------------------------------------------------------------------------- | -| Fork-review fixes (#4): pipeline symlink guard, torrential `WaitMap`, RPC secret, plugin WS auth | ✅ | `develop` + torrential tests | -| Launch hardening (#55–#59), uninstaller tests, launch-handler override | ✅ | #56 model tests, #59 UI override | -| `MetadataProvider` SPI + core consumption | ✅ | `plugin-provider.ts`, `08.plugin-system.ts` | -| `PaymentGateway` SPI + webhook dispatch | ✅ | `commerce/webhooks.ts`, `[gateway].post.ts` | -| `CloudSavePathResolver` SPI (server + desktop + SDK schema) + consumption | ✅ | `saves/resolvers.ts`, `cloudsave/patterns.post.ts` | -| `StoreScanner` SPI + desktop aggregation | ✅ | `storeImport.ts`, `useStoreImport.ts` | -| New plugin repositories (17) | ✅ | created, built, tested, pushed | -| Upstream PR 1 (platform/stability) & PR 2 (generic SPI) | ✅ | `docs/implementation/upstream-pr{1,2}-*.md`, branches `upstream/pr{1,2}-*` | +| Area | Status | Evidence | +| :----------------------------------------------------------------------------------------------- | :----- | :--------------------------------------------------------------------------------------------------------------------------------------- | +| Fork-review fixes (#4): pipeline symlink guard, torrential `WaitMap`, RPC secret, plugin WS auth | ✅ | `develop` + torrential tests | +| Launch hardening (#55–#59), uninstaller tests, launch-handler override | ✅ | #56 model tests, #59 UI override | +| `MetadataProvider` SPI + core consumption | ✅ | `plugin-provider.ts`, `08.plugin-system.ts` | +| `PaymentGateway` SPI + webhook dispatch | ✅ | `commerce/webhooks.ts`, `[gateway].post.ts` | +| `CloudSavePathResolver` SPI (server + desktop + SDK schema) + consumption | ✅ | `saves/resolvers.ts`, `cloudsave/patterns.post.ts` | +| `StoreScanner` SPI + desktop aggregation | ✅ | `storeImport.ts`, `useStoreImport.ts` | +| New plugin repositories (17) | ✅ | created, built, tested, pushed | +| Upstream PR 1 (platform/stability) & PR 2 (generic SPI) | ✅ | PRs [#497](https://github.com/Drop-OSS/drop/pull/497)/[#498](https://github.com/Drop-OSS/drop/pull/498) from `upstream-pr/*`; docs below | ## `` — Client/Server cloud saves @@ -31,20 +31,24 @@ tests and a pushed commit; `🟡` usable contract/reference implementation; ## Tranches 4–7 -| Tranche | Status | Notes | -| :--------------------------------- | :----- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| #17 Content delivery | ✅ | `downpour push` CDC chunker + zstd + memory/S3 upload; torrential LAN registry, SSDP discovery, HTTP chunk fetcher; shader-cache discovery + `drop-gamebox` index; PCGamingWiki externalized. QUIC transport remains optional 🟡. | -| #18 Handheld/Input/Overlay | 🟡 | `input` crate (mapping + backend availability) + Big Picture page + on-screen keyboard ✅; native uinput/ViGEm backends ⛔ (need `/dev/uinput`/ViGEm driver); Vulkan/DXGI overlay ⛔ (needs the Vulkan layer toolchain). | -| #8 Achievements | ✅ | Prisma models + migration, unlock API, `drop:achievement:unlock` bus; GSE `achievements.json` parser + addon bridge; RetroAchievements mapping/emission; desktop unlock toasts mounted. | -| #20/#21 Social/Federation/Commerce | ✅ | verified reviews + community page; Ed25519 receipts + webhook dispatch; federation presence; ODP signed catalog syndication; store-scanner aggregation. Store/payment provider API keys are operator-supplied (reported). | +| Tranche | Status | Notes | +| :--------------------------------- | :----- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| #17 Content delivery | ✅ | `downpour push` CDC chunker + zstd + memory/S3 upload; torrential LAN registry, SSDP discovery, HTTP chunk fetcher; shader-cache discovery + `drop-gamebox` index; PCGamingWiki externalized. QUIC is contract-only and deferred: `torrential/src/lan/mod.rs` defines the `ChunkFetcher` trait, HTTP is the implemented transport. | +| #18 Handheld/Input/Overlay | 🟡 | `input` crate (mapping + `NullBackend` availability reporting) + Big Picture page + on-screen keyboard ✅; native uinput/ViGEm backends are **not implemented** (the `BackendKind` variants are API surface only) ⛔; Vulkan/DXGI overlay ⛔ (needs the Vulkan layer toolchain). | +| #8 Achievements | ✅ | Prisma models + migration, unlock API, `drop:achievement:unlock` bus; GSE `achievements.json` parser + addon bridge; RetroAchievements mapping/emission; desktop unlock toasts mounted. | +| #20/#21 Social/Federation/Commerce | ✅ | verified reviews + community page; Ed25519 receipts + webhook dispatch; federation presence; ODP signed catalog syndication; store-scanner aggregation. Store/payment provider API keys are operator-supplied (reported). | ## Hard blockers (reported, not stubbed) - **Native virtual gamepad** — needs `/dev/uinput` access (Linux) or the ViGEm - bus driver (Windows); `input::supported_backends()` + `NullBackend` surface a + bus driver (Windows); no native `InputBackend` implementation exists yet, so + `input::supported_backends()` reports only `NullBackend`, which returns a precise `BackendUnavailable` error instead of silently no-op'ing. - **Vulkan/DXGI in-game overlay** — requires the Vulkan layer toolchain and a - DXGI hook; the `OverlaySlot` SPI and UI slots are already in place. + DXGI hook. Only the slot names (`overlay:panel`, `overlay:quick-access` in + `desktop/main/internal/plugins/types.ts`) and the client slot registry + (`ClientPluginManager`) exist; no overlay host mounts them and there is no + `OverlaySlot` SPI type. - **External provider credentials** — SteamGridDB/LaunchBox/ScreenScraper/ MobyGames API keys and Stripe/BTCPay secrets are operator-supplied. diff --git a/docs/implementation/upstream-pr1-platform-stability.md b/docs/implementation/upstream-pr1-platform-stability.md index b6f4a0675..6477559ca 100644 --- a/docs/implementation/upstream-pr1-platform-stability.md +++ b/docs/implementation/upstream-pr1-platform-stability.md @@ -1,10 +1,16 @@ # Upstream PR 1 — Platform & Stability Cleanups **Target:** `Drop-OSS/drop` -**Source:** `Heretek-Games/drop` (`upstream/pr1-platform-stability`) +**Open PR:** [#497](https://github.com/Drop-OSS/drop/pull/497) +**Source:** `Heretek-Games/drop` (`upstream-pr/platform-stability`) **Goal:** land general-purpose platform fixes with zero Heretek-domain content, so upstream can merge them independently of the plugin ecosystem. +> [!NOTE] +> The `upstream/pr1-platform-stability` branch on `Heretek-Games/drop` is a +> stale duplicate (commit `536e63ac`, no PR). The branch backing #497 is +> `upstream-pr/platform-stability`. + ## Scope ### Security & correctness (fork review #4) diff --git a/docs/implementation/upstream-pr2-generic-plugin-spi.md b/docs/implementation/upstream-pr2-generic-plugin-spi.md index 060c3546a..9286b683b 100644 --- a/docs/implementation/upstream-pr2-generic-plugin-spi.md +++ b/docs/implementation/upstream-pr2-generic-plugin-spi.md @@ -1,11 +1,17 @@ # Upstream PR 2 — Generic Plugin Runtime SPI **Target:** `Drop-OSS/drop` -**Source:** `Heretek-Games/drop` (`upstream/pr2-generic-plugin-spi`) +**Open PR:** [#498](https://github.com/Drop-OSS/drop/pull/498) +**Source:** `Heretek-Games/drop` (`upstream-pr/generic-plugin-spi`) **Goal:** land the generic, domain-agnostic plugin runtime so third parties can extend Drop without forking. Contains **zero** hardcoded piracy, emulation, VPN, or payment logic. +> [!NOTE] +> The `upstream/pr2-generic-plugin-spi` branch on `Heretek-Games/drop` is a +> stale duplicate (commit `536e63ac`, no PR). The branch backing #498 is +> `upstream-pr/generic-plugin-spi`. + ## Scope ### Runtime (`server/server/internal/plugins/`) From adede2ec80e0a3d649184910d8efa09cfe2dcd37 Mon Sep 17 00:00:00 2001 From: John Smith Date: Mon, 14 Sep 2026 10:53:41 -0400 Subject: [PATCH 203/243] fix(server): reclaim pruned save objects and cap uploads while streaming Save history pruning called deleteWithPermission on objects that are only granted `:read`, so the permission check always failed and the return value was discarded: old snapshots accumulated forever. Reclaim them with deleteAsSystem (matching screenshots) and let the caller observe the result. Also enforce saveSlotSizeLimit inside the pipeline instead of after the whole archive reaches disk, so an oversized payload is rejected with a 413 before an unbounded write. --- .../internal/saves/__tests__/saves.test.ts | 5 -- server/server/internal/saves/manager.ts | 55 ++++++++++++++----- 2 files changed, 41 insertions(+), 19 deletions(-) diff --git a/server/server/internal/saves/__tests__/saves.test.ts b/server/server/internal/saves/__tests__/saves.test.ts index cdc760076..606a8830c 100644 --- a/server/server/internal/saves/__tests__/saves.test.ts +++ b/server/server/internal/saves/__tests__/saves.test.ts @@ -70,11 +70,6 @@ const objectHandler: SaveManagerDeps["objectHandler"] = { sink.on("finish", () => objects.set(id, Buffer.concat(chunks))); return sink; }, - deleteWithPermission: async (id) => { - deleted.push(id); - objects.delete(id); - return true; - }, deleteAsSystem: async (id) => { deleted.push(id); objects.delete(id); diff --git a/server/server/internal/saves/manager.ts b/server/server/internal/saves/manager.ts index 0236b8e93..8afa2aa00 100644 --- a/server/server/internal/saves/manager.ts +++ b/server/server/internal/saves/manager.ts @@ -7,6 +7,14 @@ export interface SaveSlotRecord { historyChecksums: string[]; } +/** Raised by the measuring stream when a save exceeds `saveSlotSizeLimit`. */ +class SaveSizeLimitError extends Error { + constructor() { + super("save exceeds saveSlotSizeLimit"); + this.name = "SaveSizeLimitError"; + } +} + /** * Dependencies the save pipeline needs. Declared structurally so the manager * can be unit-tested with fakes without importing the Prisma client, the @@ -38,7 +46,6 @@ export interface SaveManagerDeps { metadata: { [key: string]: string }, permissions: string[], ): Promise; - deleteWithPermission(id: string, userId?: string): Promise; deleteAsSystem(id: string): Promise; }; settings: { @@ -58,10 +65,15 @@ export class SaveManager { userId: string, index: number, objectId: string, - ) { + ): Promise { void gameId; + void userId; void index; - await this.deps.objectHandler.deleteWithPermission(objectId, userId); + // Save objects are system-tracked (`${userId}:read`) and the id is taken + // from the caller's own slot history, so reclaim them as the system. + // `deleteWithPermission` would require a `delete` grant the object is + // deliberately never given, silently leaking every pruned snapshot. + return await this.deps.objectHandler.deleteAsSystem(objectId); } async pushSave( @@ -97,22 +109,37 @@ export class SaveManager { statusMessage: "Failed to create writing stream to storage backend.", }); + const sizeLimitMb = await this.deps.settings.get("saveSlotSizeLimit"); + const sizeLimitBytes = sizeLimitMb * 1024 * 1024; + const hashStream = createHash("sha256"); let totalBytes = 0; - stream.on("data", (chunk: Buffer) => { - hashStream.update(chunk); - totalBytes += chunk.length; - }); - await Stream.promises.pipeline(stream, newSaveStream); + // Measure while streaming so an oversized payload is rejected before the + // whole archive reaches disk, rather than after an unbounded write. + const measuringStream = new Stream.Transform({ + transform(chunk: Buffer, _encoding, callback) { + totalBytes += chunk.length; + if (totalBytes > sizeLimitBytes) { + callback(new SaveSizeLimitError()); + return; + } + hashStream.update(chunk); + callback(null, chunk); + }, + }); - const sizeLimitMb = await this.deps.settings.get("saveSlotSizeLimit"); - if (totalBytes > sizeLimitMb * 1024 * 1024) { + try { + await Stream.promises.pipeline(stream, measuringStream, newSaveStream); + } catch (error) { await this.deps.objectHandler.deleteAsSystem(newSaveObjectId); - throw createError({ - statusCode: 413, - statusMessage: "Save exceeds saveSlotSizeLimit", - }); + if (error instanceof SaveSizeLimitError) { + throw createError({ + statusCode: 413, + statusMessage: "Save exceeds saveSlotSizeLimit", + }); + } + throw error; } const hash = hashStream.digest("hex"); From d0e29530655b2b9cbe939efe7e2611104b1975da Mon Sep 17 00:00:00 2001 From: John Smith Date: Mon, 14 Sep 2026 10:53:47 -0400 Subject: [PATCH 204/243] fix(desktop): consume plugin events through the Tauri event bridge plugin_subscribe emits a Tauri event named plugin:event, but subscribe() listened with window.addEventListener and expected a DOM CustomEvent, so plugin WebSocket messages never reached their listeners. Register a real Tauri listener for plugin:event (payload {channel,data}) in the Tauri host and keep the DOM CustomEvent path as the browser/dev fallback. --- .../internal/plugins/ClientPluginManager.ts | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/desktop/main/internal/plugins/ClientPluginManager.ts b/desktop/main/internal/plugins/ClientPluginManager.ts index 6061ce400..4299e7538 100644 --- a/desktop/main/internal/plugins/ClientPluginManager.ts +++ b/desktop/main/internal/plugins/ClientPluginManager.ts @@ -1,5 +1,6 @@ import { reactive, ref } from "vue"; import { invoke } from "@tauri-apps/api/core"; +import { listen } from "@tauri-apps/api/event"; import type { AntiCheatReport, ClientPlugin, @@ -170,11 +171,43 @@ class TauriPluginWebSocket implements ClientPluginWebSocket { subscribe(channel: string, listener: (data: unknown) => void): () => void { if (isTauri()) { + let disposed = false; + let unlisten: (() => void) | undefined; + safeInvoke("plugin_subscribe", { channel }).catch((err) => { console.error(`Failed to subscribe to plugin channel ${channel}:`, err); }); + + // The Rust host forwards decoded WebSocket frames as the Tauri event + // `plugin:event`. Tauri events are not DOM events, so the payload must be + // consumed with `listen` rather than `window.addEventListener`. + void listen<{ channel?: string; data?: unknown }>( + "plugin:event", + (event) => { + if (event.payload?.channel === channel) { + listener(event.payload.data); + } + }, + ) + .then((off) => { + if (disposed) off(); + else unlisten = off; + }) + .catch((err) => { + console.error( + `Failed to listen for plugin events on ${channel}:`, + err, + ); + }); + + return () => { + disposed = true; + unlisten?.(); + unlisten = undefined; + }; } + // Browser/dev fallback: a host harness can dispatch a DOM CustomEvent. const handler = (event: Event) => { const custom = event as CustomEvent<{ channel?: string; data?: unknown }>; if (custom?.detail?.channel === channel) { From 6bcfa1b115775384c5d2e46c2a7d5e1eead78900 Mon Sep 17 00:00:00 2001 From: John Smith Date: Mon, 14 Sep 2026 10:56:01 -0400 Subject: [PATCH 205/243] docs(roadmap): correct test count and record anti-cheat SPI follow-up --- docs/implementation/roadmap-status.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/docs/implementation/roadmap-status.md b/docs/implementation/roadmap-status.md index be196ce80..8a65d01da 100644 --- a/docs/implementation/roadmap-status.md +++ b/docs/implementation/roadmap-status.md @@ -52,10 +52,19 @@ tests and a pushed commit; `🟡` usable contract/reference implementation; - **External provider credentials** — SteamGridDB/LaunchBox/ScreenScraper/ MobyGames API keys and Stripe/BTCPay secrets are operator-supplied. +## Known follow-ups + +- **Genericize anti-cheat detection in the desktop host.** `plugin_game_check_anticheat` + (`desktop/src-tauri/src/plugins.rs`) hardcodes EAC/BattlEye/Vanguard/Denuvo + names, and `ScopedGameScanner.checkAntiCheat` is part of the SPI. Moving this + knowledge into `drop-gse` (a generic "find files by name" host primitive) + requires a coordinated `drop-plugin-sdk` + `drop-gse` contract change, so it + is deferred rather than changed piecemeal. + ## Verification (all green) ```sh -pnpm --filter drop run test # 12/12 files +pnpm --filter drop run test # 13/13 files pnpm --filter drop run typecheck # 0 pnpm -C desktop/main run lint # 0 errors cargo +nightly test -p cloud_saves -p database -p input -p shader_cache From 8dde0ae64173f07c08a203136e58fdde4ba8484f Mon Sep 17 00:00:00 2001 From: John Smith Date: Mon, 14 Sep 2026 11:17:23 -0400 Subject: [PATCH 206/243] refactor(desktop): expose a generic findFiles host primitive plugin_game_check_anticheat hardcoded EAC/BattlEye/Vanguard/Denuvo names in the generic client host. Replace it with plugin_game_find_files, which returns installed paths matching caller-supplied patterns, and drop the AntiCheatReport/checkAntiCheat SPI so that domain knowledge lives in the plugin (e.g. drop-gse) rather than core. --- .../internal/plugins/ClientPluginManager.ts | 13 ++-- desktop/main/internal/plugins/types.ts | 15 ++-- desktop/src-tauri/src/lib.rs | 2 +- desktop/src-tauri/src/plugins.rs | 70 ++++++++----------- 4 files changed, 41 insertions(+), 59 deletions(-) diff --git a/desktop/main/internal/plugins/ClientPluginManager.ts b/desktop/main/internal/plugins/ClientPluginManager.ts index 4299e7538..e467a11a8 100644 --- a/desktop/main/internal/plugins/ClientPluginManager.ts +++ b/desktop/main/internal/plugins/ClientPluginManager.ts @@ -2,7 +2,6 @@ import { reactive, ref } from "vue"; import { invoke } from "@tauri-apps/api/core"; import { listen } from "@tauri-apps/api/event"; import type { - AntiCheatReport, ClientPlugin, ClientPluginContext, ClientPluginStorage, @@ -153,13 +152,13 @@ class TauriScopedGameScanner implements ScopedGameScanner { ); } - async checkAntiCheat(gameId: string): Promise { + async findFiles(gameId: string, patterns: string[]): Promise { return ( - (await safeInvoke( - "plugin_game_check_anticheat", - { gameId }, - { detected: false, files: [] }, - )) || { detected: false, files: [] } + (await safeInvoke( + "plugin_game_find_files", + { gameId, patterns }, + [], + )) || [] ); } } diff --git a/desktop/main/internal/plugins/types.ts b/desktop/main/internal/plugins/types.ts index bcb777294..45eb6a468 100644 --- a/desktop/main/internal/plugins/types.ts +++ b/desktop/main/internal/plugins/types.ts @@ -107,19 +107,16 @@ export interface ScopedGameFs { deleteFile(gameId: string, relativePath: string): Promise; } -export interface AntiCheatReport { - detected: boolean; - reason?: string; - provider?: string; - binaries?: string[]; - files?: string[]; -} - export interface ScopedGameScanner { scanExecutables( gameId: string, ): Promise>; - checkAntiCheat(gameId: string): Promise; + /** + * Returns the relative paths of installed files whose path contains any of + * the supplied patterns (case-insensitive). The host is agnostic about the + * patterns, so plugins own domain knowledge such as anti-cheat detection. + */ + findFiles(gameId: string, patterns: string[]): Promise; } export interface ClientPluginStorage { diff --git a/desktop/src-tauri/src/lib.rs b/desktop/src-tauri/src/lib.rs index c4e9448d8..280753ec0 100644 --- a/desktop/src-tauri/src/lib.rs +++ b/desktop/src-tauri/src/lib.rs @@ -269,7 +269,7 @@ pub fn run() { plugin_game_fs_exists, plugin_game_fs_delete, plugin_game_scan_executables, - plugin_game_check_anticheat, + plugin_game_find_files, plugin_register_commands, plugin_system_run, // Library diff --git a/desktop/src-tauri/src/plugins.rs b/desktop/src-tauri/src/plugins.rs index 8f5536a76..f4ca979b3 100644 --- a/desktop/src-tauri/src/plugins.rs +++ b/desktop/src-tauri/src/plugins.rs @@ -21,14 +21,6 @@ pub struct ScannedExecutable { pub size: u64, } -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct AntiCheatReport { - pub detected: bool, - pub provider: Option, - pub files: Vec, -} - /// Per-plugin allowlist of bare executable names a client plugin may run via /// `ctx.system.run`. Populated from `manifest.client.commands` by the host when /// a plugin is registered; the Tauri command layer is the enforcement point so @@ -364,51 +356,45 @@ pub async fn plugin_game_scan_executables( Ok(executables) } +/// Find files inside an installed game whose relative path contains any of the +/// supplied patterns (case-insensitive substring). The host is deliberately +/// agnostic about what the patterns mean, so domain knowledge (e.g. which paths +/// indicate a particular anti-cheat or compatibility tool) lives in the calling +/// plugin, not in core. Symlinks are never followed. #[tauri::command] -pub async fn plugin_game_check_anticheat(game_id: String) -> Result { +pub async fn plugin_game_find_files( + game_id: String, + patterns: Vec, +) -> Result, String> { let install_dir = get_game_install_dir(&game_id)?; - let mut detected_files = Vec::new(); - let mut detected_provider: Option = None; + let normalized: Vec = patterns + .into_iter() + .map(|pattern| pattern.trim().to_ascii_lowercase()) + .filter(|pattern| !pattern.is_empty() && pattern.len() <= 128) + .collect(); + if normalized.is_empty() { + return Ok(Vec::new()); + } + + let mut matches = Vec::new(); for entry in WalkDir::new(&install_dir) .into_iter() .filter_map(|e| e.ok()) { let path = entry.path(); - let path_str = path.to_string_lossy().to_lowercase(); + if !path.is_file() || path_guard::is_symlink(path) { + continue; + } - let provider = if path_str.contains("easyanticheat") - || path_str.contains("eac_server") - || path_str.contains("easyanticheat_x64.dll") - { - Some("easyanticheat") - } else if path_str.contains("battleye") - || path_str.contains("beservice") - || path_str.contains("beclient") - { - Some("battleye") - } else if path_str.contains("vgk.sys") || path_str.contains("vgc.exe") { - Some("vanguard") - } else if path_str.contains("denuvo") || path_str.contains("dbdata.dll") { - Some("denuvo") - } else { - None + let Ok(rel) = path.strip_prefix(&install_dir) else { + continue; }; - - if let Some(p) = provider - && let Ok(rel) = path.strip_prefix(&install_dir) - { - detected_files.push(rel.to_string_lossy().to_string()); - if detected_provider.is_none() { - detected_provider = Some(p.to_string()); - } + let rel_str = rel.to_string_lossy().to_ascii_lowercase(); + if normalized.iter().any(|pattern| rel_str.contains(pattern)) { + matches.push(rel.to_string_lossy().to_string()); } } - let detected = !detected_files.is_empty(); - Ok(AntiCheatReport { - detected, - provider: detected_provider, - files: detected_files, - }) + Ok(matches) } From a8c8ba5fd802bd8f53972527b2554ad35deeafb7 Mon Sep 17 00:00:00 2001 From: John Smith Date: Mon, 14 Sep 2026 11:20:20 -0400 Subject: [PATCH 207/243] docs(roadmap): record the completed findFiles anti-cheat refactor --- docs/implementation/roadmap-status.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/implementation/roadmap-status.md b/docs/implementation/roadmap-status.md index 8a65d01da..55e436181 100644 --- a/docs/implementation/roadmap-status.md +++ b/docs/implementation/roadmap-status.md @@ -54,12 +54,12 @@ tests and a pushed commit; `🟡` usable contract/reference implementation; ## Known follow-ups -- **Genericize anti-cheat detection in the desktop host.** `plugin_game_check_anticheat` - (`desktop/src-tauri/src/plugins.rs`) hardcodes EAC/BattlEye/Vanguard/Denuvo - names, and `ScopedGameScanner.checkAntiCheat` is part of the SPI. Moving this - knowledge into `drop-gse` (a generic "find files by name" host primitive) - requires a coordinated `drop-plugin-sdk` + `drop-gse` contract change, so it - is deferred rather than changed piecemeal. +- **Genericize anti-cheat detection in the desktop host — done.** The host now + exposes `plugin_game_find_files` and drops `AntiCheatReport`/`checkAntiCheat` + from the SPI; `drop-gse` owns the EAC/BattlEye/Vanguard/Denuvo patterns and + detects the capability at runtime (generic `findFiles`, legacy `checkAntiCheat` + fallback). `@droposs/plugin-sdk` documents `ScopedGameScanner.findFiles` and is + bumped to `0.5.0` (npm publish pending). ## Verification (all green) From e565d0eaee91de2ee0ae830f26fb3eed54691722 Mon Sep 17 00:00:00 2001 From: John Smith Date: Mon, 14 Sep 2026 12:24:22 -0400 Subject: [PATCH 208/243] feat(torrential): add opt-in chunk authentication for remote depots Depot chunk content was always served unauthenticated; only the manifest and /invalidate routes honored TORRENTIAL_HTTP_TOKEN. Exposing a depot to the internet (seedbox/remote demos) needs content auth. Add a DepotAuth module with constant-time token comparison and a new TORRENTIAL_REQUIRE_CHUNK_AUTH opt-in that gates the content route. It is disabled by default to preserve anonymous local downloads and fails closed when enabled without TORRENTIAL_HTTP_TOKEN. Refs Heretek-Games/drop-seedbox#6 --- AGENTS.md | 4 + torrential/src/auth.rs | 241 +++++++++++++++++++++++++++++++++++++++++ torrential/src/lib.rs | 1 + torrential/src/main.rs | 74 ++++++++----- 4 files changed, 291 insertions(+), 29 deletions(-) create mode 100644 torrential/src/auth.rs diff --git a/AGENTS.md b/AGENTS.md index a156c12bd..a259305fd 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -121,6 +121,10 @@ Source: `torrential/src/downloads/cache.rs` (wired in `serve.rs`, `state.rs`, `m - `/invalidate` and `/api/v1/depot/manifest.json` accept an optional `TORRENTIAL_HTTP_TOKEN` (`Authorization: Bearer ` or `x-torrential-token`); when unset those routes stay open for back-compat. + Chunk content (`/api/v1/depot/content/...`) can additionally require the same + token by setting `TORRENTIAL_REQUIRE_CHUNK_AUTH=true`; it is off by default so + anonymous local depots keep working, and enabling it without configuring + `TORRENTIAL_HTTP_TOKEN` fails closed (every chunk request is rejected). ### 2.5 Tauri high-DPI window architecture diff --git a/torrential/src/auth.rs b/torrential/src/auth.rs new file mode 100644 index 000000000..9518d9b3e --- /dev/null +++ b/torrential/src/auth.rs @@ -0,0 +1,241 @@ +//! Shared-secret authorization for the depot HTTP server. +//! +//! The catalog (`/api/v1/depot/manifest.json`) and cache invalidation +//! (`/invalidate`) routes have always been gated by `TORRENTIAL_HTTP_TOKEN` +//! when it is configured. Chunk *content* was intentionally left open so that +//! anonymous clients could download from a public depot. +//! +//! Exposing a depot to the internet (for example a remote/seedbox depot) makes +//! that gap a real vulnerability, so chunk authentication is now available as +//! an explicit opt-in: set `TORRENTIAL_REQUIRE_CHUNK_AUTH=true` to require the +//! shared token on every content request. Enabling it without configuring +//! `TORRENTIAL_HTTP_TOKEN` fails closed (every chunk request is rejected) +//! instead of silently allowing anonymous access. + +use axum::http::{HeaderMap, header}; + +/// Depot route authorization configuration. +#[derive(Debug, Clone, Default)] +pub struct DepotAuth { + token: Option, + require_chunk_auth: bool, +} + +impl DepotAuth { + /// Build the configuration from the process environment. + #[must_use] + pub fn from_env() -> Self { + Self::new( + std::env::var("TORRENTIAL_HTTP_TOKEN").ok(), + std::env::var("TORRENTIAL_REQUIRE_CHUNK_AUTH") + .ok() + .as_deref(), + ) + } + + /// Build a configuration from explicit values (used by tests). + #[must_use] + pub fn new(token: Option, require_chunk_auth: Option<&str>) -> Self { + let token = token + .map(|value| value.trim().to_string()) + .filter(|value| !value.is_empty()); + Self { + token, + require_chunk_auth: parse_bool(require_chunk_auth), + } + } + + /// Whether chunk content requests must present the shared token. + #[must_use] + pub fn requires_chunk_auth(&self) -> bool { + self.require_chunk_auth + } + + /// Gate the catalog and invalidation routes. + /// + /// Enforced only when `TORRENTIAL_HTTP_TOKEN` is configured, preserving the + /// historical behaviour for deployments that never set it. + /// + /// # Errors + /// + /// Returns [`AuthError::Rejected`] when a token is configured but the + /// request did not present a matching token. + pub fn authorize_catalog(&self, headers: &HeaderMap) -> Result<(), AuthError> { + let Some(expected) = self.token.as_deref() else { + return Ok(()); + }; + Self::verify(headers, expected) + } + + /// Gate chunk content routes. + /// + /// When the opt-in is disabled this always allows the request (backwards + /// compatible). When enabled it is fail-closed: a missing configured token + /// rejects every request. + /// + /// # Errors + /// + /// Returns [`AuthError::NotConfigured`] when chunk auth is enabled but no + /// token is configured, or [`AuthError::Rejected`] when the presented token + /// is missing or does not match. + pub fn authorize_chunk(&self, headers: &HeaderMap) -> Result<(), AuthError> { + if !self.require_chunk_auth { + return Ok(()); + } + let Some(expected) = self.token.as_deref() else { + return Err(AuthError::NotConfigured); + }; + Self::verify(headers, expected) + } + + fn verify(headers: &HeaderMap, expected: &str) -> Result<(), AuthError> { + match presented_token(headers) { + Some(presented) if constant_time_eq(presented, expected) => Ok(()), + _ => Err(AuthError::Rejected), + } + } +} + +/// Reason a depot request was rejected, for logging and status mapping. +#[derive(Debug, PartialEq, Eq)] +pub enum AuthError { + /// A token is required but none was presented, or it did not match. + Rejected, + /// Chunk auth was enabled but `TORRENTIAL_HTTP_TOKEN` is unset. + NotConfigured, +} + +/// Extract the presented shared token from the `Authorization: Bearer ` +/// header or the legacy `x-torrential-token` header. +pub fn presented_token(headers: &HeaderMap) -> Option<&str> { + headers + .get(header::AUTHORIZATION) + .and_then(|value| value.to_str().ok()) + .and_then(|value| value.strip_prefix("Bearer ")) + .or_else(|| { + headers + .get("x-torrential-token") + .and_then(|value| value.to_str().ok()) + }) +} + +fn parse_bool(value: Option<&str>) -> bool { + match value.map(|value| value.trim().to_ascii_lowercase()) { + Some(value) => matches!(value.as_str(), "1" | "true" | "yes" | "on"), + None => false, + } +} + +/// Compare two secrets without early-exit on the first differing byte. +fn constant_time_eq(a: &str, b: &str) -> bool { + let a = a.as_bytes(); + let b = b.as_bytes(); + if a.len() != b.len() { + return false; + } + let mut diff = 0u8; + for (x, y) in a.iter().zip(b.iter()) { + diff |= x ^ y; + } + diff == 0 +} + +#[cfg(test)] +mod tests { + use super::*; + use axum::http::HeaderValue; + + fn headers_with(name: &'static str, value: &'static str) -> HeaderMap { + let mut headers = HeaderMap::new(); + headers.insert( + axum::http::HeaderName::from_static(name), + HeaderValue::from_static(value), + ); + headers + } + + #[test] + fn catalog_open_without_token() { + let auth = DepotAuth::new(None, None); + assert!(auth.authorize_catalog(&HeaderMap::new()).is_ok()); + } + + #[test] + fn catalog_requires_token_when_configured() { + let auth = DepotAuth::new(Some("s3cret".into()), None); + assert_eq!( + auth.authorize_catalog(&HeaderMap::new()), + Err(AuthError::Rejected) + ); + assert!( + auth.authorize_catalog(&headers_with("authorization", "Bearer s3cret")) + .is_ok() + ); + assert!( + auth.authorize_catalog(&headers_with("x-torrential-token", "s3cret")) + .is_ok() + ); + assert_eq!( + auth.authorize_catalog(&headers_with("authorization", "Bearer nope")), + Err(AuthError::Rejected) + ); + } + + #[test] + fn chunk_open_by_default() { + let auth = DepotAuth::new(Some("s3cret".into()), None); + assert!(!auth.requires_chunk_auth()); + assert!(auth.authorize_chunk(&HeaderMap::new()).is_ok()); + } + + #[test] + fn chunk_requires_token_when_opted_in() { + let auth = DepotAuth::new(Some("s3cret".into()), Some("true")); + assert!(auth.requires_chunk_auth()); + assert_eq!( + auth.authorize_chunk(&HeaderMap::new()), + Err(AuthError::Rejected) + ); + assert!( + auth.authorize_chunk(&headers_with("authorization", "Bearer s3cret")) + .is_ok() + ); + } + + #[test] + fn chunk_fails_closed_without_configured_token() { + let auth = DepotAuth::new(None, Some("1")); + assert_eq!( + auth.authorize_chunk(&headers_with("authorization", "Bearer anything")), + Err(AuthError::NotConfigured) + ); + } + + #[test] + fn blank_token_is_treated_as_unset() { + let auth = DepotAuth::new(Some(" ".into()), Some("on")); + assert_eq!( + auth.authorize_chunk(&headers_with("x-torrential-token", "")), + Err(AuthError::NotConfigured) + ); + } + + #[test] + fn parse_bool_accepts_common_truthy_values() { + for value in ["1", "true", "TRUE", "Yes", "on", " on "] { + assert!(parse_bool(Some(value)), "{value} should be truthy"); + } + for value in ["0", "false", "no", "off", "", "maybe"] { + assert!(!parse_bool(Some(value)), "{value} should be falsy"); + } + assert!(!parse_bool(None)); + } + + #[test] + fn constant_time_eq_matches_semantics() { + assert!(constant_time_eq("abc", "abc")); + assert!(!constant_time_eq("abc", "abz")); + assert!(!constant_time_eq("abc", "abcd")); + assert!(constant_time_eq("", "")); + } +} diff --git a/torrential/src/lib.rs b/torrential/src/lib.rs index f2b8ff84f..bbdeb922e 100644 --- a/torrential/src/lib.rs +++ b/torrential/src/lib.rs @@ -1,4 +1,5 @@ use tokio::sync::Semaphore; +pub mod auth; pub mod conversions; pub mod downloads; pub mod droplet; diff --git a/torrential/src/main.rs b/torrential/src/main.rs index 60c69689c..90255258d 100644 --- a/torrential/src/main.rs +++ b/torrential/src/main.rs @@ -6,7 +6,7 @@ use std::{ }; use axum::{ - Router, + Extension, Router, extract::Request, http::StatusCode, middleware::{self, Next}, @@ -18,6 +18,7 @@ use log::{info, warn}; use simple_logger::SimpleLogger; use tokio::{runtime::Handle, spawn, time}; use torrential::{ + auth::DepotAuth, downloads::{cache::ChunkCache, handlers, serve}, server::create_drop_server, state::AppState, @@ -85,55 +86,70 @@ async fn main() -> anyhow::Result<()> { Ok(()) } -/// Gate the catalog and cache-invalidation routes behind a shared token. +/// Gate the catalog and cache-invalidation routes behind the shared token. /// /// When `TORRENTIAL_HTTP_TOKEN` is unset the routes remain open (backwards /// compatible for single-tenant deployments); setting it requires callers to /// present it as `Authorization: Bearer ` or `x-torrential-token`. -async fn require_depot_token(request: Request, next: Next) -> Result { - let Ok(expected) = std::env::var("TORRENTIAL_HTTP_TOKEN") else { - return Ok(next.run(request).await); - }; - let expected = expected.trim(); - if expected.is_empty() { - return Ok(next.run(request).await); +async fn authorize_catalog( + Extension(auth): Extension>, + request: Request, + next: Next, +) -> Result { + match auth.authorize_catalog(request.headers()) { + Ok(()) => Ok(next.run(request).await), + Err(error) => { + warn!("rejected depot catalog request: {error:?}"); + Err(StatusCode::UNAUTHORIZED) + } } +} - let presented = request - .headers() - .get(axum::http::header::AUTHORIZATION) - .and_then(|value| value.to_str().ok()) - .and_then(|value| value.strip_prefix("Bearer ")) - .or_else(|| { - request - .headers() - .get("x-torrential-token") - .and_then(|value| value.to_str().ok()) - }); - - if presented.is_some_and(|token| token == expected) { - Ok(next.run(request).await) - } else { - Err(StatusCode::UNAUTHORIZED) +/// Gate chunk content behind the shared token when chunk auth is opted in. +/// +/// Disabled by default (see [`DepotAuth`]); once +/// `TORRENTIAL_REQUIRE_CHUNK_AUTH=true` is set, unauthenticated chunk requests +/// are rejected and a missing token configuration fails closed. +async fn authorize_chunk( + Extension(auth): Extension>, + request: Request, + next: Next, +) -> Result { + match auth.authorize_chunk(request.headers()) { + Ok(()) => Ok(next.run(request).await), + Err(error) => { + warn!("rejected depot chunk request: {error:?}"); + Err(StatusCode::UNAUTHORIZED) + } } } fn setup_app(shared_state: Arc) -> Router { + let auth = Arc::new(DepotAuth::from_env()); + if auth.requires_chunk_auth() { + info!("depot chunk authentication is enabled (TORRENTIAL_REQUIRE_CHUNK_AUTH)"); + } + // The manifest catalog and cache invalidation are not needed by anonymous // depot downloads, so they can require a token. - let protected = Router::new() + let catalog = Router::new() .route("/api/v1/depot/manifest.json", get(handlers::manifest)) .route("/invalidate", post(handlers::invalidate)) - .route_layer(middleware::from_fn(require_depot_token)); + .route_layer(middleware::from_fn(authorize_catalog)); - Router::new() + let content = Router::new() .route( "/api/v1/depot/content/{game_id}/{version_name}/{chunk_id}", get(serve::serve_file), ) + .route_layer(middleware::from_fn(authorize_chunk)); + + Router::new() .route("/api/v1/depot/speedtest", get(handlers::speedtest)) .route("/healthcheck", get(handlers::healthcheck)) - .merge(protected) + .merge(content) + .merge(catalog) + .layer(Extension(auth)) .with_state(shared_state) } From d0aa35f1ce42b4b4f6b879a63aec8cd383e72628 Mon Sep 17 00:00:00 2001 From: John Smith Date: Mon, 14 Sep 2026 12:31:36 -0400 Subject: [PATCH 209/243] feat(torrential): verify LAN peer chunk SHA-256 before commit LAN peers are untrusted, so peer-fetched chunks must match the checksum in the local droplet manifest before they are committed to disk (#17). - Add verify_chunk_sha256 (constant-shape 64-hex validation, timing-neutral full-hash comparison) and fetch_verified_from_best_peer, which only returns bytes once they hash to the expected digest. - 2 new tests: checksum matching/rejection and a tampered-peer fetch. --- docs/implementation/roadmap-status.md | 12 +-- torrential/src/lan/mod.rs | 102 ++++++++++++++++++++++++++ 2 files changed, 108 insertions(+), 6 deletions(-) diff --git a/docs/implementation/roadmap-status.md b/docs/implementation/roadmap-status.md index 55e436181..3684f787b 100644 --- a/docs/implementation/roadmap-status.md +++ b/docs/implementation/roadmap-status.md @@ -31,12 +31,12 @@ tests and a pushed commit; `🟡` usable contract/reference implementation; ## Tranches 4–7 -| Tranche | Status | Notes | -| :--------------------------------- | :----- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| #17 Content delivery | ✅ | `downpour push` CDC chunker + zstd + memory/S3 upload; torrential LAN registry, SSDP discovery, HTTP chunk fetcher; shader-cache discovery + `drop-gamebox` index; PCGamingWiki externalized. QUIC is contract-only and deferred: `torrential/src/lan/mod.rs` defines the `ChunkFetcher` trait, HTTP is the implemented transport. | -| #18 Handheld/Input/Overlay | 🟡 | `input` crate (mapping + `NullBackend` availability reporting) + Big Picture page + on-screen keyboard ✅; native uinput/ViGEm backends are **not implemented** (the `BackendKind` variants are API surface only) ⛔; Vulkan/DXGI overlay ⛔ (needs the Vulkan layer toolchain). | -| #8 Achievements | ✅ | Prisma models + migration, unlock API, `drop:achievement:unlock` bus; GSE `achievements.json` parser + addon bridge; RetroAchievements mapping/emission; desktop unlock toasts mounted. | -| #20/#21 Social/Federation/Commerce | ✅ | verified reviews + community page; Ed25519 receipts + webhook dispatch; federation presence; ODP signed catalog syndication; store-scanner aggregation. Store/payment provider API keys are operator-supplied (reported). | +| Tranche | Status | Notes | +| :--------------------------------- | :----- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| #17 Content delivery | ✅ | `downpour push` CDC chunker + zstd + memory/S3 upload; torrential LAN registry, SSDP discovery, HTTP chunk fetcher with peer-chunk SHA-256 verification (`verify_chunk_sha256` / `fetch_verified_from_best_peer`); shader-cache discovery + `drop-gamebox` index; PCGamingWiki externalized. QUIC is contract-only and deferred: `torrential/src/lan/mod.rs` defines the `ChunkFetcher` trait, HTTP is the implemented transport. | +| #18 Handheld/Input/Overlay | 🟡 | `input` crate (mapping + `NullBackend` availability reporting) + Big Picture page + on-screen keyboard ✅; native uinput/ViGEm backends are **not implemented** (the `BackendKind` variants are API surface only) ⛔; Vulkan/DXGI overlay ⛔ (needs the Vulkan layer toolchain). | +| #8 Achievements | ✅ | Prisma models + migration, unlock API, `drop:achievement:unlock` bus; GSE `achievements.json` parser + addon bridge; RetroAchievements mapping/emission; desktop unlock toasts mounted. | +| #20/#21 Social/Federation/Commerce | ✅ | verified reviews + community page; Ed25519 receipts + webhook dispatch; federation presence; ODP signed catalog syndication; store-scanner aggregation. Store/payment provider API keys are operator-supplied (reported). | ## Hard blockers (reported, not stubbed) diff --git a/torrential/src/lan/mod.rs b/torrential/src/lan/mod.rs index ee51ee61d..93f32d267 100644 --- a/torrential/src/lan/mod.rs +++ b/torrential/src/lan/mod.rs @@ -11,6 +11,8 @@ pub mod ssdp; use std::net::SocketAddr; use std::time::{Duration, Instant}; +use sha2::{Digest, Sha256}; + /// A peer that advertises the same depot content on the local network. #[derive(Debug, Clone, PartialEq, Eq)] pub struct LanPeer { @@ -116,6 +118,55 @@ pub fn fetch_from_best_peer( Some(fetcher.fetch(&peer, content_id, chunk_index)) } +/// Verify that `bytes` hash to `expected_hex` (a lowercase/uppercase 64-char +/// SHA-256 digest). +/// +/// This is the integrity check a client must run **before committing** a +/// peer-fetched chunk to disk: LAN peers are untrusted, so bytes are only ever +/// trusted after they match the checksum from the local droplet manifest. +/// +/// # Errors +/// Returns an error string when the expected digest is malformed or the bytes +/// do not match it. +pub fn verify_chunk_sha256(bytes: &[u8], expected_hex: &str) -> Result<(), String> { + let expected = expected_hex.trim(); + if expected.len() != 64 || !expected.bytes().all(|b| b.is_ascii_hexdigit()) { + return Err(format!("invalid expected checksum '{expected_hex}'")); + } + let digest = to_hex(Sha256::digest(bytes).as_slice()); + if digest.eq_ignore_ascii_case(expected) { + Ok(()) + } else { + Err(format!( + "chunk checksum mismatch: expected {expected}, got {digest}" + )) + } +} + +/// Fetch a chunk from the best live peer and only return it once its SHA-256 +/// matches the manifest checksum. Returns `None` when no live peer exists. +pub fn fetch_verified_from_best_peer( + registry: &LanPeerRegistry, + now: Instant, + fetcher: &F, + content_id: &str, + chunk_index: u64, + expected_sha256_hex: &str, +) -> Option, String>> { + Some( + fetch_from_best_peer(registry, now, fetcher, content_id, chunk_index)? + .and_then(|bytes| verify_chunk_sha256(&bytes, expected_sha256_hex).map(|()| bytes)), + ) +} + +fn to_hex(bytes: &[u8]) -> String { + use std::fmt::Write as _; + bytes.iter().fold(String::new(), |mut out, byte| { + let _ = write!(out, "{byte:02x}"); + out + }) +} + /// The client-to-client HTTP URL for a chunk on a LAN peer. #[must_use] pub fn chunk_url(peer: &LanPeer, content_id: &str, chunk_index: u64) -> String { @@ -221,6 +272,57 @@ mod tests { ); } + #[test] + fn verifies_matching_chunk_checksums() { + let digest = to_hex(Sha256::digest(b"hello").as_slice()); + assert!(verify_chunk_sha256(b"hello", &digest).is_ok()); + assert!(verify_chunk_sha256(b"hello", &digest.to_uppercase()).is_ok()); + assert!(verify_chunk_sha256(b"hello", "not-a-valid-checksum").is_err()); + assert!(verify_chunk_sha256(b"world", &digest).is_err()); + } + + #[test] + fn rejects_a_chunk_that_does_not_match_the_manifest_checksum() { + struct FixedFetcher(Vec); + impl ChunkFetcher for FixedFetcher { + fn fetch( + &self, + _peer: &LanPeer, + _content_id: &str, + _chunk_index: u64, + ) -> Result, String> { + Ok(self.0.clone()) + } + } + + let mut registry = LanPeerRegistry::new(Duration::from_secs(30)); + let now = Instant::now(); + registry.observe(peer("fast", 1, 100), now); + + let good = b"trusted-bytes".to_vec(); + let digest = to_hex(Sha256::digest(&good).as_slice()); + + let accepted = fetch_verified_from_best_peer( + ®istry, + now, + &FixedFetcher(good.clone()), + "depot", + 0, + &digest, + ); + assert_eq!(accepted, Some(Ok(good))); + + let tampered = fetch_verified_from_best_peer( + ®istry, + now, + &FixedFetcher(b"evil-bytes".to_vec()), + "depot", + 0, + &digest, + ); + assert!(matches!(tampered, Some(Err(_)))); + } + fn peer(id: &str, rtt: u32, kibps: u32) -> LanPeer { LanPeer { peer_id: id.to_string(), From c13bfeba024d4f0db5d7dc9d32c5d0d5140966c0 Mon Sep 17 00:00:00 2001 From: John Smith Date: Mon, 14 Sep 2026 12:33:48 -0400 Subject: [PATCH 210/243] feat(server): add leaderboard API and best-score tracking Phase 3 / #8 remaining leaderboard work: - AchievementManager gains upsertLeaderboard, submitScore (keeps the best result per user; asc keeps lowest, desc keeps highest), and listLeaderboard (ordered, bounded top-N). - Client routes GET/POST /api/v1/client/leaderboards/:gameid/:key; the POST is client-authenticated and validates a finite score. - 4 new manager tests (best-score retention, ascending order, unknown leaderboard, non-finite score). --- .../client/leaderboards/[gameid]/[key].get.ts | 21 +++ .../leaderboards/[gameid]/[key].post.ts | 42 +++++ .../__tests__/achievements.test.ts | 153 ++++++++++++++++++ server/server/internal/achievements/index.ts | 5 + .../server/internal/achievements/manager.ts | 152 +++++++++++++++++ 5 files changed, 373 insertions(+) create mode 100644 server/server/api/v1/client/leaderboards/[gameid]/[key].get.ts create mode 100644 server/server/api/v1/client/leaderboards/[gameid]/[key].post.ts diff --git a/server/server/api/v1/client/leaderboards/[gameid]/[key].get.ts b/server/server/api/v1/client/leaderboards/[gameid]/[key].get.ts new file mode 100644 index 000000000..27a804d59 --- /dev/null +++ b/server/server/api/v1/client/leaderboards/[gameid]/[key].get.ts @@ -0,0 +1,21 @@ +import { defineClientEventHandler } from "~/server/internal/clients/event-handler"; +import achievementManager from "~/server/internal/achievements"; + +export default defineClientEventHandler(async (h3) => { + const gameId = getRouterParam(h3, "gameid"); + const key = getRouterParam(h3, "key"); + if (!gameId || !key) + throw createError({ + statusCode: 400, + statusMessage: "gameId and key are required", + }); + + const limitParam = getQuery(h3).limit; + const parsedLimit = Number(limitParam); + const limit = + Number.isFinite(parsedLimit) && parsedLimit > 0 + ? Math.min(Math.floor(parsedLimit), 500) + : 50; + + return await achievementManager.listLeaderboard(gameId, key, limit); +}); diff --git a/server/server/api/v1/client/leaderboards/[gameid]/[key].post.ts b/server/server/api/v1/client/leaderboards/[gameid]/[key].post.ts new file mode 100644 index 000000000..6ad8a95ec --- /dev/null +++ b/server/server/api/v1/client/leaderboards/[gameid]/[key].post.ts @@ -0,0 +1,42 @@ +import { defineClientEventHandler } from "~/server/internal/clients/event-handler"; +import achievementManager from "~/server/internal/achievements"; +import type { LeaderboardSort } from "~/server/internal/achievements"; + +interface SubmitScoreBody { + score?: number; + name?: string; + sortOrder?: LeaderboardSort; +} + +export default defineClientEventHandler(async (h3, { fetchUser }) => { + const user = await fetchUser(); + const gameId = getRouterParam(h3, "gameid"); + const key = getRouterParam(h3, "key"); + if (!gameId || !key) + throw createError({ + statusCode: 400, + statusMessage: "gameId and key are required", + }); + + const body = ((await readBody(h3)) ?? {}) as SubmitScoreBody; + if (typeof body.score !== "number" || !Number.isFinite(body.score)) { + throw createError({ + statusCode: 400, + statusMessage: "score must be a finite number", + }); + } + + const result = await achievementManager.submitScore(user.id, { + gameId, + key, + score: body.score, + name: body.name ?? key, + sortOrder: body.sortOrder, + }); + + return { + improved: result.improved, + entry: result.entry, + leaderboard: result.leaderboard, + }; +}); diff --git a/server/server/internal/achievements/__tests__/achievements.test.ts b/server/server/internal/achievements/__tests__/achievements.test.ts index 0ab05c81b..958eae34f 100644 --- a/server/server/internal/achievements/__tests__/achievements.test.ts +++ b/server/server/internal/achievements/__tests__/achievements.test.ts @@ -6,12 +6,16 @@ import { summarizeProgress, type AchievementRecord, type AchievementsDeps, + type LeaderboardEntryRecord, + type LeaderboardRecord, } from "../manager"; function createHarness() { const definitions = new Map(); const unlocks = new Set(); const events: Array<{ channel: string; event: unknown }> = []; + const leaderboards = new Map(); + const leaderboardEntries = new Map(); let nextId = 1; const keyOf = (gameId: string, key: string) => `${gameId}:${key}`; @@ -53,6 +57,63 @@ function createHarness() { return { achievementId: data.achievementId, unlockedAt: new Date() }; }, }, + leaderboard: { + findUnique: async ({ where }) => + leaderboards.get( + keyOf(where.gameId_key.gameId, where.gameId_key.key), + ) ?? null, + create: async ({ data }) => { + const record: LeaderboardRecord = { + id: `lb-${nextId++}`, + gameId: data.gameId, + key: data.key, + name: data.name, + sortOrder: data.sortOrder, + }; + leaderboards.set(keyOf(data.gameId, data.key), record); + return record; + }, + }, + leaderboardEntry: { + findMany: async ({ where, orderBy, take }) => { + const entries = [...leaderboardEntries.values()].filter( + (entry) => entry.leaderboardId === where.leaderboardId, + ); + entries.sort((a, b) => + orderBy.score === "asc" ? a.score - b.score : b.score - a.score, + ); + return take ? entries.slice(0, take) : entries; + }, + findUnique: async ({ where }) => + leaderboardEntries.get( + `${where.leaderboardId_userId.leaderboardId}:${where.leaderboardId_userId.userId}`, + ) ?? null, + create: async ({ data }) => { + const record: LeaderboardEntryRecord = { + leaderboardId: data.leaderboardId, + userId: data.userId, + score: data.score, + updatedAt: new Date(), + }; + leaderboardEntries.set( + `${data.leaderboardId}:${data.userId}`, + record, + ); + return record; + }, + update: async ({ where, data }) => { + const key = `${where.leaderboardId_userId.leaderboardId}:${where.leaderboardId_userId.userId}`; + const existing = leaderboardEntries.get(key); + if (!existing) throw new Error("missing entry"); + const record: LeaderboardEntryRecord = { + ...existing, + score: data.score, + updatedAt: new Date(), + }; + leaderboardEntries.set(key, record); + return record; + }, + }, }, emit: (channel, event) => events.push({ channel, event }), }; @@ -163,3 +224,95 @@ test("summarizeProgress ignores unlocks without definitions", () => { unlockedPoints: 10, }); }); + +test("submitScore keeps only the best result per user", async () => { + const { manager } = createHarness(); + + const first = await manager.submitScore("user-1", { + gameId: "game-1", + key: "high-score", + name: "High Score", + score: 100, + }); + assert.equal(first.improved, true); + + const worse = await manager.submitScore("user-1", { + gameId: "game-1", + key: "high-score", + name: "High Score", + score: 50, + }); + assert.equal(worse.improved, false); + assert.equal(worse.entry.score, 100); + + const better = await manager.submitScore("user-1", { + gameId: "game-1", + key: "high-score", + name: "High Score", + score: 150, + }); + assert.equal(better.improved, true); + assert.equal(better.entry.score, 150); + + const { entries } = await manager.listLeaderboard("game-1", "high-score"); + assert.deepEqual( + entries.map((entry) => entry.score), + [150], + ); +}); + +test("asc leaderboards keep the lowest score and list ascending", async () => { + const { manager } = createHarness(); + + await manager.submitScore("user-1", { + gameId: "game-1", + key: "fastest", + name: "Fastest Time", + sortOrder: "asc", + score: 30, + }); + await manager.submitScore("user-2", { + gameId: "game-1", + key: "fastest", + name: "Fastest Time", + sortOrder: "asc", + score: 10, + }); + const slower = await manager.submitScore("user-1", { + gameId: "game-1", + key: "fastest", + name: "Fastest Time", + sortOrder: "asc", + score: 45, + }); + assert.equal(slower.improved, false); + assert.equal(slower.entry.score, 30); + + const { entries } = await manager.listLeaderboard("game-1", "fastest"); + assert.deepEqual( + entries.map((entry) => entry.score), + [10, 30], + ); +}); + +test("listLeaderboard rejects unknown leaderboards", async () => { + const { manager } = createHarness(); + await assert.rejects( + () => manager.listLeaderboard("game-1", "missing"), + /Unknown leaderboard/, + ); +}); + +test("submitScore rejects non-finite scores", async () => { + const { manager } = createHarness(); + await assert.rejects( + () => + manager.submitScore("user-1", { + gameId: "game-1", + key: "score", + name: "Score", + score: Number.NaN, + }), + /finite/, + ); +}); diff --git a/server/server/internal/achievements/index.ts b/server/server/internal/achievements/index.ts index e115c5c80..ca2472ed7 100644 --- a/server/server/internal/achievements/index.ts +++ b/server/server/internal/achievements/index.ts @@ -16,6 +16,11 @@ export { type AchievementProgress, type AchievementRecord, type AchievementsDeps, + type LeaderboardEntryRecord, + type LeaderboardInput, + type LeaderboardRecord, + type LeaderboardSort, + type SubmitScoreInput, type UserAchievementRecord, } from "./manager"; export default achievementManager; diff --git a/server/server/internal/achievements/manager.ts b/server/server/internal/achievements/manager.ts index 14cf450a8..b9e18eacb 100644 --- a/server/server/internal/achievements/manager.ts +++ b/server/server/internal/achievements/manager.ts @@ -33,6 +33,34 @@ export interface AchievementProgress { unlockedPoints: number; } +export type LeaderboardSort = "asc" | "desc"; + +export interface LeaderboardRecord { + id: string; + gameId: string; + key: string; + name: string; + sortOrder: string; +} + +export interface LeaderboardEntryRecord { + leaderboardId: string; + userId: string; + score: number; + updatedAt: Date; +} + +export interface LeaderboardInput { + gameId: string; + key: string; + name: string; + sortOrder?: LeaderboardSort; +} + +export interface SubmitScoreInput extends LeaderboardInput { + score: number; +} + /** Dependency surface kept structural so the manager is unit-testable. */ export interface AchievementsDeps { prisma: { @@ -55,6 +83,40 @@ export interface AchievementsDeps { data: { userId: string; achievementId: string }; }): Promise; }; + leaderboard: { + findUnique(args: { + where: { gameId_key: { gameId: string; key: string } }; + }): Promise; + create(args: { + data: { + gameId: string; + key: string; + name: string; + sortOrder: string; + }; + }): Promise; + }; + leaderboardEntry: { + findMany(args: { + where: { leaderboardId: string }; + orderBy: { score: LeaderboardSort }; + take?: number; + }): Promise; + findUnique(args: { + where: { + leaderboardId_userId: { leaderboardId: string; userId: string }; + }; + }): Promise; + create(args: { + data: { leaderboardId: string; userId: string; score: number }; + }): Promise; + update(args: { + where: { + leaderboardId_userId: { leaderboardId: string; userId: string }; + }; + data: { score: number }; + }): Promise; + }; }; emit: (channel: string, event: unknown) => void; } @@ -164,4 +226,94 @@ export class AchievementManager { progress: summarizeProgress(definitions, new Set(unlockedIds)), }; } + + /** Idempotently registers a leaderboard for a game. */ + async upsertLeaderboard(input: LeaderboardInput): Promise { + const existing = await this.deps.prisma.leaderboard.findUnique({ + where: { gameId_key: { gameId: input.gameId, key: input.key } }, + }); + if (existing) return existing; + return this.deps.prisma.leaderboard.create({ + data: { + gameId: input.gameId, + key: input.key, + name: input.name, + sortOrder: input.sortOrder ?? "desc", + }, + }); + } + + /** + * Records a score, keeping only the user's best result. `asc` leaderboards + * (e.g. fastest time) keep the lowest score; `desc` keep the highest. + */ + async submitScore( + userId: string, + input: SubmitScoreInput, + ): Promise<{ + leaderboard: LeaderboardRecord; + entry: LeaderboardEntryRecord; + improved: boolean; + }> { + if (!Number.isFinite(input.score)) { + throw createError({ + statusCode: 400, + statusMessage: "score must be a finite number", + }); + } + const leaderboard = await this.upsertLeaderboard(input); + const existing = await this.deps.prisma.leaderboardEntry.findUnique({ + where: { + leaderboardId_userId: { leaderboardId: leaderboard.id, userId }, + }, + }); + if (!existing) { + const entry = await this.deps.prisma.leaderboardEntry.create({ + data: { leaderboardId: leaderboard.id, userId, score: input.score }, + }); + return { leaderboard, entry, improved: true }; + } + const better = + leaderboard.sortOrder === "asc" + ? input.score < existing.score + : input.score > existing.score; + if (!better) { + return { leaderboard, entry: existing, improved: false }; + } + const entry = await this.deps.prisma.leaderboardEntry.update({ + where: { + leaderboardId_userId: { leaderboardId: leaderboard.id, userId }, + }, + data: { score: input.score }, + }); + return { leaderboard, entry, improved: true }; + } + + /** Lists a leaderboard's top entries in its configured order. */ + async listLeaderboard( + gameId: string, + key: string, + limit = 50, + ): Promise<{ + leaderboard: LeaderboardRecord; + entries: LeaderboardEntryRecord[]; + }> { + const leaderboard = await this.deps.prisma.leaderboard.findUnique({ + where: { gameId_key: { gameId, key } }, + }); + if (!leaderboard) { + throw createError({ + statusCode: 404, + statusMessage: "Unknown leaderboard", + }); + } + const sortOrder: LeaderboardSort = + leaderboard.sortOrder === "asc" ? "asc" : "desc"; + const entries = await this.deps.prisma.leaderboardEntry.findMany({ + where: { leaderboardId: leaderboard.id }, + orderBy: { score: sortOrder }, + take: limit, + }); + return { leaderboard, entries }; + } } From e9a126e089670fe5017e555fbc5c3b44813210c7 Mon Sep 17 00:00:00 2001 From: John Smith Date: Mon, 14 Sep 2026 12:36:34 -0400 Subject: [PATCH 211/243] feat(cli): binary delta and branch/depot metadata for downpour push Phase 1 / #17 remaining developer-publishing work: - push --base reuses chunks already published and only writes/uploads changed content (content-defined chunking keeps the unchanged prefix); PushSummary reports new vs reused chunks. - push --branch/--depot record release branch and target depot in the manifest. - 2 new tests: delta reuse across an append, and branch/depot metadata. --- cli/src/cli.rs | 9 ++ cli/src/commands/push/mod.rs | 142 ++++++++++++++++++++++++-- cli/src/main.rs | 20 +++- docs/implementation/roadmap-status.md | 12 +-- 4 files changed, 165 insertions(+), 18 deletions(-) diff --git a/cli/src/cli.rs b/cli/src/cli.rs index 1199e6ffe..f19d23b5d 100644 --- a/cli/src/cli.rs +++ b/cli/src/cli.rs @@ -44,6 +44,15 @@ pub enum Commands { /// Object-store key prefix for the upload #[arg(long, default_value_t = String::new())] prefix: String, + /// Release branch (main, beta, nightly); recorded in the manifest + #[arg(long)] + branch: Option, + /// Multi-platform depot (windows, linux, assets); recorded in the manifest + #[arg(long)] + depot: Option, + /// Previous manifest to diff against for a binary delta + #[arg(long)] + base: Option, }, } diff --git a/cli/src/commands/push/mod.rs b/cli/src/commands/push/mod.rs index 8be743ee5..994daf171 100644 --- a/cli/src/commands/push/mod.rs +++ b/cli/src/commands/push/mod.rs @@ -11,7 +11,7 @@ use std::fs; use std::path::{Path, PathBuf}; use anyhow::{Context, Result}; -use serde::Serialize; +use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; use walkdir::WalkDir; @@ -26,46 +26,93 @@ fn hex_digest(bytes: &[u8]) -> String { out } +#[derive(Debug, Default, Clone)] +pub struct PushOptions { + /// Release branch the artifacts belong to (e.g. `main`, `beta`). + pub branch: Option, + /// Multi-platform depot the artifacts belong to (e.g. `windows`, `assets`). + pub depot: Option, + /// Previous manifest to diff against; chunks already present there are + /// reused instead of being written and re-uploaded. + pub base_manifest: Option, +} + #[derive(Debug, Serialize)] pub struct PushSummary { pub files: usize, pub chunks: usize, pub bytes_in: u64, pub bytes_out: u64, + /// Chunks whose digest was already present in `base_manifest`. + pub reused_chunks: usize, + /// Chunks newly written (and therefore uploaded). + pub new_chunks: usize, } -#[derive(Debug, Serialize)] +#[derive(Debug, Serialize, Deserialize)] struct ChunkRef { sha256: String, original_len: usize, compressed_len: usize, } -#[derive(Debug, Serialize)] +#[derive(Debug, Serialize, Deserialize)] struct ManifestEntry { path: String, chunks: Vec, } -#[derive(Debug, Serialize)] +#[derive(Debug, Serialize, Deserialize)] struct PushManifest { version: u32, - chunker: &'static str, + chunker: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + branch: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + depot: Option, entries: Vec, } +/// Read the set of chunk digests already published by a previous manifest. +fn read_base_chunks(manifest_path: &Path) -> Result> { + let bytes = fs::read(manifest_path) + .with_context(|| format!("reading base manifest {}", manifest_path.display()))?; + let manifest: PushManifest = serde_json::from_slice(&bytes).context("parsing base manifest")?; + let mut digests = std::collections::HashSet::new(); + for entry in manifest.entries { + for chunk in entry.chunks { + digests.insert(chunk.sha256); + } + } + Ok(digests) +} + /// Chunks `path` into `out/chunks` and writes `out/manifest.json`. +#[cfg(test)] pub fn run(path: &Path, out: &Path) -> Result { + run_with_options(path, out, &PushOptions::default()) +} + +/// Chunks `path` and writes a manifest, reusing chunks from an optional base +/// manifest so only changed content is emitted (and later uploaded). +pub fn run_with_options(path: &Path, out: &Path, options: &PushOptions) -> Result { let chunker = Chunker::default(); let chunk_dir = out.join("chunks"); fs::create_dir_all(&chunk_dir).context("creating chunk output directory")?; + let base_chunks = match &options.base_manifest { + Some(base) => read_base_chunks(base)?, + None => std::collections::HashSet::new(), + }; + let mut entries = Vec::new(); let mut summary = PushSummary { files: 0, chunks: 0, bytes_in: 0, bytes_out: 0, + reused_chunks: 0, + new_chunks: 0, }; let mut written: std::collections::HashSet = std::collections::HashSet::new(); @@ -87,13 +134,20 @@ pub fn run(path: &Path, out: &Path) -> Result { let mut chunks = Vec::new(); for chunk in chunker.split(&data) { - let compressed = zstd::bulk::compress(chunk, 3).context("compressing chunk")?; let digest = hex_digest(chunk); - let chunk_path = chunk_dir.join(&digest); - if !written.contains(&digest) { + let already_published = base_chunks.contains(&digest); + let compressed = zstd::bulk::compress(chunk, 3).context("compressing chunk")?; + + if already_published { + summary.reused_chunks += 1; + } else if !written.contains(&digest) { + let chunk_path = chunk_dir.join(&digest); fs::write(&chunk_path, &compressed).context("writing chunk")?; written.insert(digest.clone()); + summary.new_chunks += 1; summary.bytes_out += compressed.len() as u64; + } else { + summary.reused_chunks += 1; } summary.chunks += 1; chunks.push(ChunkRef { @@ -111,7 +165,9 @@ pub fn run(path: &Path, out: &Path) -> Result { let manifest = PushManifest { version: 1, - chunker: "buzhash-cdc-v1", + chunker: "buzhash-cdc-v1".to_string(), + branch: options.branch.clone(), + depot: options.depot.clone(), entries, }; let manifest_path = out.join("manifest.json"); @@ -263,4 +319,72 @@ mod tests { } assert_eq!(rebuilt, payload_a); } + + #[test] + fn push_reuses_chunks_from_a_base_manifest() { + let input = tempdir().unwrap(); + let base = tempdir().unwrap(); + let out = tempdir().unwrap(); + + let original = vec![11u8; 200_000]; + fs::write(input.path().join("data.bin"), &original).unwrap(); + let first = run(input.path(), base.path()).unwrap(); + assert!(first.new_chunks >= 1); + + // Unchanged content: every chunk should be reused, nothing rewritten. + let unchanged = run_with_options( + input.path(), + out.path(), + &PushOptions { + base_manifest: Some(base.path().join("manifest.json")), + ..Default::default() + }, + ) + .unwrap(); + assert_eq!(unchanged.new_chunks, 0); + assert!(unchanged.reused_chunks >= 1); + assert_eq!(unchanged.bytes_out, 0); + + // Appended content: the unchanged prefix is reused, the tail is new. + let mut modified = original.clone(); + modified.extend_from_slice(&[99u8; 5_000]); + fs::write(input.path().join("data.bin"), &modified).unwrap(); + let delta = run_with_options( + input.path(), + out.path(), + &PushOptions { + base_manifest: Some(base.path().join("manifest.json")), + ..Default::default() + }, + ) + .unwrap(); + assert!( + delta.reused_chunks >= 1, + "unchanged prefix should be reused" + ); + assert!(delta.new_chunks >= 1, "appended content should be new"); + } + + #[test] + fn push_records_branch_and_depot_in_the_manifest() { + let input = tempdir().unwrap(); + let out = tempdir().unwrap(); + fs::write(input.path().join("a.bin"), vec![1u8; 100]).unwrap(); + + run_with_options( + input.path(), + out.path(), + &PushOptions { + branch: Some("beta".to_string()), + depot: Some("windows".to_string()), + base_manifest: None, + }, + ) + .unwrap(); + + let manifest: serde_json::Value = + serde_json::from_slice(&fs::read(out.path().join("manifest.json")).unwrap()).unwrap(); + assert_eq!(manifest["branch"], "beta"); + assert_eq!(manifest["depot"], "windows"); + } } diff --git a/cli/src/main.rs b/cli/src/main.rs index ab72a6726..3ab72572d 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -33,14 +33,28 @@ async fn main() -> anyhow::Result<()> { out, upload, prefix, + branch, + depot, + base, } => { - let summary = crate::commands::push::run( + let options = crate::commands::push::PushOptions { + branch, + depot, + base_manifest: base.map(std::path::PathBuf::from), + }; + let summary = crate::commands::push::run_with_options( std::path::Path::new(&path), std::path::Path::new(&out), + &options, )?; println!( - "pushed {} file(s), {} chunk(s), {} -> {} bytes", - summary.files, summary.chunks, summary.bytes_in, summary.bytes_out + "pushed {} file(s), {} chunk(s) ({} new, {} reused), {} -> {} bytes", + summary.files, + summary.chunks, + summary.new_chunks, + summary.reused_chunks, + summary.bytes_in, + summary.bytes_out ); if let Some(scheme) = upload { diff --git a/docs/implementation/roadmap-status.md b/docs/implementation/roadmap-status.md index 3684f787b..318e7ca20 100644 --- a/docs/implementation/roadmap-status.md +++ b/docs/implementation/roadmap-status.md @@ -31,12 +31,12 @@ tests and a pushed commit; `🟡` usable contract/reference implementation; ## Tranches 4–7 -| Tranche | Status | Notes | -| :--------------------------------- | :----- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| #17 Content delivery | ✅ | `downpour push` CDC chunker + zstd + memory/S3 upload; torrential LAN registry, SSDP discovery, HTTP chunk fetcher with peer-chunk SHA-256 verification (`verify_chunk_sha256` / `fetch_verified_from_best_peer`); shader-cache discovery + `drop-gamebox` index; PCGamingWiki externalized. QUIC is contract-only and deferred: `torrential/src/lan/mod.rs` defines the `ChunkFetcher` trait, HTTP is the implemented transport. | -| #18 Handheld/Input/Overlay | 🟡 | `input` crate (mapping + `NullBackend` availability reporting) + Big Picture page + on-screen keyboard ✅; native uinput/ViGEm backends are **not implemented** (the `BackendKind` variants are API surface only) ⛔; Vulkan/DXGI overlay ⛔ (needs the Vulkan layer toolchain). | -| #8 Achievements | ✅ | Prisma models + migration, unlock API, `drop:achievement:unlock` bus; GSE `achievements.json` parser + addon bridge; RetroAchievements mapping/emission; desktop unlock toasts mounted. | -| #20/#21 Social/Federation/Commerce | ✅ | verified reviews + community page; Ed25519 receipts + webhook dispatch; federation presence; ODP signed catalog syndication; store-scanner aggregation. Store/payment provider API keys are operator-supplied (reported). | +| Tranche | Status | Notes | +| :--------------------------------- | :----- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| #17 Content delivery | ✅ | `downpour push` CDC chunker + zstd + memory/S3 upload with binary delta reuse (`--base`) and `--branch`/`--depot` manifest metadata; torrential LAN registry, SSDP discovery, HTTP chunk fetcher with peer-chunk SHA-256 verification (`verify_chunk_sha256` / `fetch_verified_from_best_peer`); shader-cache discovery + `drop-gamebox` index; PCGamingWiki externalized. QUIC is contract-only and deferred: `torrential/src/lan/mod.rs` defines the `ChunkFetcher` trait, HTTP is the implemented transport. | +| #18 Handheld/Input/Overlay | 🟡 | `input` crate (mapping + `NullBackend` availability reporting) + Big Picture page + on-screen keyboard ✅; native uinput/ViGEm backends are **not implemented** (the `BackendKind` variants are API surface only) ⛔; Vulkan/DXGI overlay ⛔ (needs the Vulkan layer toolchain). | +| #8 Achievements | ✅ | Prisma models + migration, unlock API, `drop:achievement:unlock` bus; GSE `achievements.json` parser + addon bridge; RetroAchievements mapping/emission; desktop unlock toasts mounted. | +| #20/#21 Social/Federation/Commerce | ✅ | verified reviews + community page; Ed25519 receipts + webhook dispatch; federation presence; ODP signed catalog syndication; store-scanner aggregation. Store/payment provider API keys are operator-supplied (reported). | ## Hard blockers (reported, not stubbed) From 24a216917605f53b78d680260d422739d18b2731 Mon Sep 17 00:00:00 2001 From: John Smith Date: Mon, 14 Sep 2026 12:39:36 -0400 Subject: [PATCH 212/243] feat(server): add Drop Workshop mod registry and subscriptions Phase 4 / #20 Workshop (previously not started): - Prisma models Mod / ModRelease / ModSubscription with a migration and back-relations on Game and User. - mod.json manifest validation (required id/name/semver, dependency array, safe relative file paths) and a WorkshopManager with publish, list/get, subscribe/unsubscribe/list, semver latest resolution, and a dependency install-plan resolver with cycle detection. - Public routes GET /api/v1/workshop/:gameid/mods[/:key]; client routes for subscriptions and admin-only publishing. - 7 manager/manifest tests (validation, upsert, latest semver, subscriptions, install plan). --- .../20260917000000_add_workshop/migration.sql | 66 ++++ server/prisma/models/content.prisma | 1 + server/prisma/models/user.prisma | 1 + server/prisma/models/workshop.prisma | 56 ++++ .../[gameid]/mods/[key]/subscribe.delete.ts | 16 + .../[gameid]/mods/[key]/subscribe.post.ts | 26 ++ .../workshop/[gameid]/mods/index.post.ts | 38 +++ .../v1/client/workshop/subscriptions.get.ts | 8 + .../v1/workshop/[gameid]/mods/[key].get.ts | 13 + .../v1/workshop/[gameid]/mods/index.get.ts | 10 + .../workshop/__tests__/workshop.test.ts | 286 ++++++++++++++++ server/server/internal/workshop/index.ts | 12 + server/server/internal/workshop/manager.ts | 313 ++++++++++++++++++ server/server/internal/workshop/manifest.ts | 128 +++++++ 14 files changed, 974 insertions(+) create mode 100644 server/prisma/migrations/20260917000000_add_workshop/migration.sql create mode 100644 server/prisma/models/workshop.prisma create mode 100644 server/server/api/v1/client/workshop/[gameid]/mods/[key]/subscribe.delete.ts create mode 100644 server/server/api/v1/client/workshop/[gameid]/mods/[key]/subscribe.post.ts create mode 100644 server/server/api/v1/client/workshop/[gameid]/mods/index.post.ts create mode 100644 server/server/api/v1/client/workshop/subscriptions.get.ts create mode 100644 server/server/api/v1/workshop/[gameid]/mods/[key].get.ts create mode 100644 server/server/api/v1/workshop/[gameid]/mods/index.get.ts create mode 100644 server/server/internal/workshop/__tests__/workshop.test.ts create mode 100644 server/server/internal/workshop/index.ts create mode 100644 server/server/internal/workshop/manager.ts create mode 100644 server/server/internal/workshop/manifest.ts diff --git a/server/prisma/migrations/20260917000000_add_workshop/migration.sql b/server/prisma/migrations/20260917000000_add_workshop/migration.sql new file mode 100644 index 000000000..8ed8121f4 --- /dev/null +++ b/server/prisma/migrations/20260917000000_add_workshop/migration.sql @@ -0,0 +1,66 @@ +-- CreateTable +CREATE TABLE "Mod" ( + "id" TEXT NOT NULL, + "gameId" TEXT NOT NULL, + "key" TEXT NOT NULL, + "name" TEXT NOT NULL, + "summary" TEXT NOT NULL DEFAULT '', + "author" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "Mod_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "ModRelease" ( + "id" TEXT NOT NULL, + "modId" TEXT NOT NULL, + "version" TEXT NOT NULL, + "manifest" TEXT NOT NULL, + "downloadUrl" TEXT, + "checksum" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "ModRelease_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "ModSubscription" ( + "id" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "modId" TEXT NOT NULL, + "pinnedVersion" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "ModSubscription_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "Mod_gameId_key_key" ON "Mod"("gameId", "key"); + +-- CreateIndex +CREATE INDEX "Mod_gameId_idx" ON "Mod"("gameId"); + +-- CreateIndex +CREATE UNIQUE INDEX "ModRelease_modId_version_key" ON "ModRelease"("modId", "version"); + +-- CreateIndex +CREATE INDEX "ModRelease_modId_idx" ON "ModRelease"("modId"); + +-- CreateIndex +CREATE UNIQUE INDEX "ModSubscription_userId_modId_key" ON "ModSubscription"("userId", "modId"); + +-- CreateIndex +CREATE INDEX "ModSubscription_userId_idx" ON "ModSubscription"("userId"); + +-- AddForeignKey +ALTER TABLE "Mod" ADD CONSTRAINT "Mod_gameId_fkey" FOREIGN KEY ("gameId") REFERENCES "Game"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ModRelease" ADD CONSTRAINT "ModRelease_modId_fkey" FOREIGN KEY ("modId") REFERENCES "Mod"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ModSubscription" ADD CONSTRAINT "ModSubscription_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ModSubscription" ADD CONSTRAINT "ModSubscription_modId_fkey" FOREIGN KEY ("modId") REFERENCES "Mod"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/server/prisma/models/content.prisma b/server/prisma/models/content.prisma index 6714c8705..02f93f7f2 100644 --- a/server/prisma/models/content.prisma +++ b/server/prisma/models/content.prisma @@ -63,6 +63,7 @@ model Game { achievements AchievementDefinition[] leaderboards Leaderboard[] reviews GameReview[] + mods Mod[] developers Company[] @relation(name: "developers") publishers Company[] @relation(name: "publishers") diff --git a/server/prisma/models/user.prisma b/server/prisma/models/user.prisma index aed039ab0..1136cbe6d 100644 --- a/server/prisma/models/user.prisma +++ b/server/prisma/models/user.prisma @@ -26,6 +26,7 @@ model User { achievements UserAchievement[] leaderboardEntries LeaderboardEntry[] reviews GameReview[] + modSubscriptions ModSubscription[] groups UserGroup[] } diff --git a/server/prisma/models/workshop.prisma b/server/prisma/models/workshop.prisma new file mode 100644 index 000000000..00ae994b2 --- /dev/null +++ b/server/prisma/models/workshop.prisma @@ -0,0 +1,56 @@ +// The Drop Workshop: open mod repository (#20 / Phase 4). + +model Mod { + id String @id @default(uuid()) + + gameId String + game Game @relation(fields: [gameId], references: [id], onDelete: Cascade) + + // Stable identifier from the mod manifest (`mod.json.id`). + key String + name String + summary String @default("") + author String? + + createdAt DateTime @default(now()) + + releases ModRelease[] + subscriptions ModSubscription[] + + @@unique([gameId, key]) + @@index([gameId]) +} + +model ModRelease { + id String @id @default(uuid()) + + modId String + mod Mod @relation(fields: [modId], references: [id], onDelete: Cascade) + + version String + // JSON-encoded, validated `mod.json` manifest. + manifest String + downloadUrl String? + checksum String? + + createdAt DateTime @default(now()) + + @@unique([modId, version]) + @@index([modId]) +} + +model ModSubscription { + id String @id @default(uuid()) + + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + modId String + mod Mod @relation(fields: [modId], references: [id], onDelete: Cascade) + + pinnedVersion String? + createdAt DateTime @default(now()) + + @@unique([userId, modId]) + @@index([userId]) +} diff --git a/server/server/api/v1/client/workshop/[gameid]/mods/[key]/subscribe.delete.ts b/server/server/api/v1/client/workshop/[gameid]/mods/[key]/subscribe.delete.ts new file mode 100644 index 000000000..4f444a1c5 --- /dev/null +++ b/server/server/api/v1/client/workshop/[gameid]/mods/[key]/subscribe.delete.ts @@ -0,0 +1,16 @@ +import { defineClientEventHandler } from "~/server/internal/clients/event-handler"; +import workshopManager from "~/server/internal/workshop"; + +export default defineClientEventHandler(async (h3, { fetchUser }) => { + const user = await fetchUser(); + const gameId = getRouterParam(h3, "gameid"); + const key = getRouterParam(h3, "key"); + if (!gameId || !key) + throw createError({ + statusCode: 400, + statusMessage: "gameId and key are required", + }); + + const removed = await workshopManager.unsubscribe(user.id, gameId, key); + return { success: true, removed }; +}); diff --git a/server/server/api/v1/client/workshop/[gameid]/mods/[key]/subscribe.post.ts b/server/server/api/v1/client/workshop/[gameid]/mods/[key]/subscribe.post.ts new file mode 100644 index 000000000..1eb798d50 --- /dev/null +++ b/server/server/api/v1/client/workshop/[gameid]/mods/[key]/subscribe.post.ts @@ -0,0 +1,26 @@ +import { defineClientEventHandler } from "~/server/internal/clients/event-handler"; +import workshopManager from "~/server/internal/workshop"; + +interface SubscribeBody { + version?: string; +} + +export default defineClientEventHandler(async (h3, { fetchUser }) => { + const user = await fetchUser(); + const gameId = getRouterParam(h3, "gameid"); + const key = getRouterParam(h3, "key"); + if (!gameId || !key) + throw createError({ + statusCode: 400, + statusMessage: "gameId and key are required", + }); + + const body = ((await readBody(h3)) ?? {}) as SubscribeBody; + const subscription = await workshopManager.subscribe( + user.id, + gameId, + key, + body.version, + ); + return { success: true, subscription }; +}); diff --git a/server/server/api/v1/client/workshop/[gameid]/mods/index.post.ts b/server/server/api/v1/client/workshop/[gameid]/mods/index.post.ts new file mode 100644 index 000000000..b704c9c2d --- /dev/null +++ b/server/server/api/v1/client/workshop/[gameid]/mods/index.post.ts @@ -0,0 +1,38 @@ +import { defineClientEventHandler } from "~/server/internal/clients/event-handler"; +import workshopManager from "~/server/internal/workshop"; + +interface PublishBody { + manifest?: unknown; + summary?: string; + downloadUrl?: string; + checksum?: string; +} + +export default defineClientEventHandler(async (h3, { fetchUser }) => { + const user = await fetchUser(); + if (!user.admin) { + throw createError({ + statusCode: 403, + statusMessage: "Admin access required to publish mods", + }); + } + const gameId = getRouterParam(h3, "gameid"); + if (!gameId) + throw createError({ statusCode: 400, statusMessage: "gameId is required" }); + + const body = ((await readBody(h3)) ?? {}) as PublishBody; + if (!body.manifest) + throw createError({ + statusCode: 400, + statusMessage: "manifest is required", + }); + + const result = await workshopManager.publish({ + gameId, + manifest: body.manifest, + summary: body.summary, + downloadUrl: body.downloadUrl, + checksum: body.checksum, + }); + return { success: true, mod: result.mod, release: result.release }; +}); diff --git a/server/server/api/v1/client/workshop/subscriptions.get.ts b/server/server/api/v1/client/workshop/subscriptions.get.ts new file mode 100644 index 000000000..1e2a7ec8f --- /dev/null +++ b/server/server/api/v1/client/workshop/subscriptions.get.ts @@ -0,0 +1,8 @@ +import { defineClientEventHandler } from "~/server/internal/clients/event-handler"; +import workshopManager from "~/server/internal/workshop"; + +export default defineClientEventHandler(async (_h3, { fetchUser }) => { + const user = await fetchUser(); + const subscriptions = await workshopManager.listSubscriptions(user.id); + return { subscriptions, count: subscriptions.length }; +}); diff --git a/server/server/api/v1/workshop/[gameid]/mods/[key].get.ts b/server/server/api/v1/workshop/[gameid]/mods/[key].get.ts new file mode 100644 index 000000000..abaf17cd0 --- /dev/null +++ b/server/server/api/v1/workshop/[gameid]/mods/[key].get.ts @@ -0,0 +1,13 @@ +import workshopManager from "~/server/internal/workshop"; + +export default defineEventHandler(async (h3) => { + const gameId = getRouterParam(h3, "gameid"); + const key = getRouterParam(h3, "key"); + if (!gameId || !key) + throw createError({ + statusCode: 400, + statusMessage: "gameId and key are required", + }); + + return await workshopManager.getMod(gameId, key); +}); diff --git a/server/server/api/v1/workshop/[gameid]/mods/index.get.ts b/server/server/api/v1/workshop/[gameid]/mods/index.get.ts new file mode 100644 index 000000000..da4925fcb --- /dev/null +++ b/server/server/api/v1/workshop/[gameid]/mods/index.get.ts @@ -0,0 +1,10 @@ +import workshopManager from "~/server/internal/workshop"; + +export default defineEventHandler(async (h3) => { + const gameId = getRouterParam(h3, "gameid"); + if (!gameId) + throw createError({ statusCode: 400, statusMessage: "gameId is required" }); + + const mods = await workshopManager.listMods(gameId); + return { mods, count: mods.length }; +}); diff --git a/server/server/internal/workshop/__tests__/workshop.test.ts b/server/server/internal/workshop/__tests__/workshop.test.ts new file mode 100644 index 000000000..8226678dd --- /dev/null +++ b/server/server/internal/workshop/__tests__/workshop.test.ts @@ -0,0 +1,286 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { + WorkshopManager, + compareVersions, + parseModManifest, + resolveInstallPlan, + isSafeModPath, + type ModRecord, + type ModReleaseRecord, + type ModSubscriptionRecord, + type WorkshopDeps, +} from "../index"; + +function createHarness() { + const mods = new Map(); + const releases: ModReleaseRecord[] = []; + const subs = new Map(); + let next = 1; + + const gameKey = (gameId: string, key: string) => `${gameId}:${key}`; + const subKey = (userId: string, modId: string) => `${userId}:${modId}`; + + const deps: WorkshopDeps = { + prisma: { + mod: { + findUnique: async ({ where }) => + mods.get(gameKey(where.gameId_key.gameId, where.gameId_key.key)) ?? + null, + create: async ({ data }) => { + const record: ModRecord = { + id: `mod-${next++}`, + gameId: data.gameId, + key: data.key, + name: data.name, + summary: data.summary, + author: data.author ?? null, + createdAt: new Date(), + }; + mods.set(gameKey(data.gameId, data.key), record); + return record; + }, + update: async ({ where, data }) => { + const entry = [...mods.values()].find((m) => m.id === where.id); + if (!entry) throw new Error("missing mod"); + const updated: ModRecord = { ...entry, ...data }; + mods.set(gameKey(updated.gameId, updated.key), updated); + return updated; + }, + findMany: async ({ where }) => + [...mods.values()].filter((m) => m.gameId === where.gameId), + }, + modRelease: { + findUnique: async ({ where }) => + releases.find( + (r) => + r.modId === where.modId_version.modId && + r.version === where.modId_version.version, + ) ?? null, + create: async ({ data }) => { + const record: ModReleaseRecord = { + id: `rel-${next++}`, + modId: data.modId, + version: data.version, + manifest: data.manifest, + downloadUrl: data.downloadUrl ?? null, + checksum: data.checksum ?? null, + createdAt: new Date(), + }; + releases.push(record); + return record; + }, + findMany: async ({ where }) => + releases.filter((r) => r.modId === where.modId), + }, + modSubscription: { + findUnique: async ({ where }) => + subs.get( + subKey(where.userId_modId.userId, where.userId_modId.modId), + ) ?? null, + create: async ({ data }) => { + const record: ModSubscriptionRecord = { + userId: data.userId, + modId: data.modId, + pinnedVersion: data.pinnedVersion ?? null, + createdAt: new Date(), + }; + subs.set(subKey(data.userId, data.modId), record); + return record; + }, + update: async ({ where, data }) => { + const key = subKey( + where.userId_modId.userId, + where.userId_modId.modId, + ); + const entry = subs.get(key); + if (!entry) throw new Error("missing subscription"); + const updated: ModSubscriptionRecord = { + ...entry, + pinnedVersion: data.pinnedVersion ?? null, + }; + subs.set(key, updated); + return updated; + }, + delete: async ({ where }) => { + const key = subKey( + where.userId_modId.userId, + where.userId_modId.modId, + ); + const entry = subs.get(key); + if (!entry) throw new Error("missing subscription"); + subs.delete(key); + return entry; + }, + findMany: async ({ where }) => + [...subs.values()].filter((s) => s.userId === where.userId), + }, + }, + }; + + return { manager: new WorkshopManager(deps), mods, releases, subs }; +} + +const baseManifest = { + id: "hi-res-textures", + name: "Hi-Res Textures", + version: "1.0.0", + author: "modder", + description: "4K texture pack", + files: ["textures/pack.pak"], +}; + +test("parseModManifest validates required fields and paths", () => { + const manifest = parseModManifest(baseManifest); + assert.equal(manifest.id, "hi-res-textures"); + assert.deepEqual(manifest.files, ["textures/pack.pak"]); + + assert.throws(() => parseModManifest({ ...baseManifest, id: "" }), /id is/); + assert.throws( + () => parseModManifest({ ...baseManifest, version: "1.0" }), + /semver/, + ); + assert.throws( + () => parseModManifest({ ...baseManifest, files: ["../escape.pak"] }), + /unsafe file path/, + ); + assert.throws( + () => parseModManifest({ ...baseManifest, dependencies: "nope" }), + /dependencies must be an array/, + ); +}); + +test("isSafeModPath rejects absolute and traversing paths", () => { + assert.equal(isSafeModPath("a/b.pak"), true); + assert.equal(isSafeModPath("/etc/passwd"), false); + assert.equal(isSafeModPath(String.raw`C:\Windows\x`), false); + assert.equal(isSafeModPath("a/../b"), false); +}); + +test("compareVersions orders numeric cores", () => { + assert.equal(compareVersions("1.2.0", "1.10.0"), -1); + assert.equal(compareVersions("2.0.0", "1.9.9"), 1); + assert.equal(compareVersions("1.0.0", "1.0.0"), 0); +}); + +test("publish upserts the mod and its release", async () => { + const { manager } = createHarness(); + + const first = await manager.publish({ + gameId: "game-1", + manifest: baseManifest, + }); + assert.equal(first.mod.key, "hi-res-textures"); + assert.equal(first.release.version, "1.0.0"); + + // Same version is idempotent; same mod with a new name updates in place. + const second = await manager.publish({ + gameId: "game-1", + manifest: { ...baseManifest, name: "Hi-Res Textures 2" }, + }); + assert.equal(second.mod.id, first.mod.id); + assert.equal(second.mod.name, "Hi-Res Textures 2"); + assert.equal(second.release.id, first.release.id); + + const mods = await manager.listMods("game-1"); + assert.equal(mods.length, 1); +}); + +test("getMod returns releases and the latest semver", async () => { + const { manager } = createHarness(); + await manager.publish({ + gameId: "game-1", + manifest: { ...baseManifest, version: "1.0.0" }, + }); + await manager.publish({ + gameId: "game-1", + manifest: { ...baseManifest, version: "1.10.0" }, + }); + + const { latest, releases } = await manager.getMod( + "game-1", + "hi-res-textures", + ); + assert.equal(releases.length, 2); + assert.equal(latest?.version, "1.10.0"); + + await assert.rejects( + () => manager.getMod("game-1", "missing"), + /Unknown mod/, + ); +}); + +test("subscriptions can be added, pinned, listed and removed", async () => { + const { manager } = createHarness(); + await manager.publish({ gameId: "game-1", manifest: baseManifest }); + + const created = await manager.subscribe( + "user-1", + "game-1", + "hi-res-textures", + ); + assert.equal(created.pinnedVersion, null); + + const pinned = await manager.subscribe( + "user-1", + "game-1", + "hi-res-textures", + "1.0.0", + ); + assert.equal(pinned.pinnedVersion, "1.0.0"); + + const list = await manager.listSubscriptions("user-1"); + assert.equal(list.length, 1); + + assert.equal( + await manager.unsubscribe("user-1", "game-1", "hi-res-textures"), + true, + ); + assert.equal( + await manager.unsubscribe("user-1", "game-1", "hi-res-textures"), + false, + ); +}); + +test("resolveInstallPlan orders dependencies first and reports gaps", () => { + const lookup = (id: string) => + ({ + lib: { + id: "lib", + name: "Lib", + version: "1.0.0", + dependencies: [], + files: [], + }, + pack: { + id: "pack", + name: "Pack", + version: "1.0.0", + dependencies: [{ id: "lib" }, { id: "missing" }], + files: [], + }, + })[id]; + + const plan = resolveInstallPlan(lookup("pack")!, lookup); + assert.deepEqual(plan.order, ["lib", "pack"]); + assert.deepEqual(plan.missing, ["missing"]); + + const cyclic = (id: string) => + ({ + a: { + id: "a", + name: "A", + version: "1.0.0", + dependencies: [{ id: "b" }], + files: [], + }, + b: { + id: "b", + name: "B", + version: "1.0.0", + dependencies: [{ id: "a" }], + files: [], + }, + })[id]; + assert.throws(() => resolveInstallPlan(cyclic("a")!, cyclic), /cycle/); +}); diff --git a/server/server/internal/workshop/index.ts b/server/server/internal/workshop/index.ts new file mode 100644 index 000000000..174830aac --- /dev/null +++ b/server/server/internal/workshop/index.ts @@ -0,0 +1,12 @@ +import prisma from "../db/database"; +import { WorkshopManager, type WorkshopDeps } from "./manager"; + +const deps: WorkshopDeps = { + prisma: prisma as unknown as WorkshopDeps["prisma"], +}; + +export const workshopManager = new WorkshopManager(deps); +export { WorkshopManager }; +export * from "./manifest"; +export * from "./manager"; +export default workshopManager; diff --git a/server/server/internal/workshop/manager.ts b/server/server/internal/workshop/manager.ts new file mode 100644 index 000000000..ea12f54ee --- /dev/null +++ b/server/server/internal/workshop/manager.ts @@ -0,0 +1,313 @@ +import { createError } from "h3"; +import { parseModManifest, type ModManifest } from "./manifest"; + +export interface ModRecord { + id: string; + gameId: string; + key: string; + name: string; + summary: string; + author?: string | null; + createdAt: Date; +} + +export interface ModReleaseRecord { + id: string; + modId: string; + version: string; + manifest: string; + downloadUrl?: string | null; + checksum?: string | null; + createdAt: Date; +} + +export interface ModSubscriptionRecord { + userId: string; + modId: string; + pinnedVersion?: string | null; + createdAt: Date; +} + +export interface PublishModInput { + gameId: string; + manifest: unknown; + summary?: string; + downloadUrl?: string; + checksum?: string; +} + +/** Dependency surface kept structural so the manager is unit-testable. */ +export interface WorkshopDeps { + prisma: { + mod: { + findUnique(args: { + where: { gameId_key: { gameId: string; key: string } }; + }): Promise; + create(args: { + data: { + gameId: string; + key: string; + name: string; + summary: string; + author?: string; + }; + }): Promise; + update(args: { + where: { id: string }; + data: { name: string; summary: string; author?: string | null }; + }): Promise; + findMany(args: { + where: { gameId: string }; + orderBy?: { createdAt: "asc" | "desc" }; + }): Promise; + }; + modRelease: { + findUnique(args: { + where: { modId_version: { modId: string; version: string } }; + }): Promise; + create(args: { + data: { + modId: string; + version: string; + manifest: string; + downloadUrl?: string; + checksum?: string; + }; + }): Promise; + findMany(args: { + where: { modId: string }; + orderBy?: { createdAt: "asc" | "desc" }; + }): Promise; + }; + modSubscription: { + findUnique(args: { + where: { userId_modId: { userId: string; modId: string } }; + }): Promise; + create(args: { + data: { userId: string; modId: string; pinnedVersion?: string }; + }): Promise; + update(args: { + where: { userId_modId: { userId: string; modId: string } }; + data: { pinnedVersion?: string | null }; + }): Promise; + delete(args: { + where: { userId_modId: { userId: string; modId: string } }; + }): Promise; + findMany(args: { + where: { userId: string }; + }): Promise; + }; + }; +} + +/** + * Compare two semver-ish versions by numeric core. Prerelease/build metadata is + * ignored beyond string comparison, which is sufficient for "pick latest". + */ +export function compareVersions(a: string, b: string): number { + const core = (value: string) => value.split(/[-+]/, 1)[0].split("."); + const left = core(a); + const right = core(b); + for (let i = 0; i < Math.max(left.length, right.length); i++) { + const l = Number(left[i] ?? 0); + const r = Number(right[i] ?? 0); + if (l !== r) return l < r ? -1 : 1; + } + return 0; +} + +/** Highest semver release, or `null` when there are none. */ +export function latestRelease( + releases: ModReleaseRecord[], +): ModReleaseRecord | null { + let latest: ModReleaseRecord | null = null; + for (const release of releases) { + if (!latest || compareVersions(release.version, latest.version) > 0) { + latest = release; + } + } + return latest; +} + +/** + * Resolve the install order for a manifest's dependency graph (dependencies + * before dependents). Missing ids are reported; a cycle is rejected. + */ +export function resolveInstallPlan( + manifest: ModManifest, + lookup: (id: string) => ModManifest | undefined, +): { order: string[]; missing: string[] } { + const order: string[] = []; + const missing: string[] = []; + const visiting = new Set(); + const visited = new Set(); + + const visit = (current: ModManifest): void => { + if (visited.has(current.id)) return; + if (visiting.has(current.id)) { + throw createError({ + statusCode: 400, + statusMessage: `Dependency cycle detected at '${current.id}'`, + }); + } + visiting.add(current.id); + for (const dependency of current.dependencies) { + const resolved = lookup(dependency.id); + if (!resolved) { + if (!missing.includes(dependency.id)) missing.push(dependency.id); + continue; + } + visit(resolved); + } + visiting.delete(current.id); + visited.add(current.id); + order.push(current.id); + }; + + visit(manifest); + return { order, missing }; +} + +export class WorkshopManager { + constructor(private readonly deps: WorkshopDeps) {} + + /** Registers or updates a mod and its release from a validated manifest. */ + async publish(input: PublishModInput): Promise<{ + mod: ModRecord; + release: ModReleaseRecord; + manifest: ModManifest; + }> { + const manifest = parseModManifest(input.manifest); + const summary = input.summary ?? manifest.description ?? ""; + const existing = await this.deps.prisma.mod.findUnique({ + where: { gameId_key: { gameId: input.gameId, key: manifest.id } }, + }); + const mod = existing + ? await this.deps.prisma.mod.update({ + where: { id: existing.id }, + data: { + name: manifest.name, + summary, + author: manifest.author ?? null, + }, + }) + : await this.deps.prisma.mod.create({ + data: { + gameId: input.gameId, + key: manifest.id, + name: manifest.name, + summary, + ...(manifest.author ? { author: manifest.author } : {}), + }, + }); + + const existingRelease = await this.deps.prisma.modRelease.findUnique({ + where: { modId_version: { modId: mod.id, version: manifest.version } }, + }); + const release = + existingRelease ?? + (await this.deps.prisma.modRelease.create({ + data: { + modId: mod.id, + version: manifest.version, + manifest: JSON.stringify(manifest), + ...(input.downloadUrl ? { downloadUrl: input.downloadUrl } : {}), + ...(input.checksum ? { checksum: input.checksum } : {}), + }, + })); + + return { mod, release, manifest }; + } + + async listMods(gameId: string): Promise { + return this.deps.prisma.mod.findMany({ + where: { gameId }, + orderBy: { createdAt: "asc" }, + }); + } + + /** A mod with its releases (newest first) and resolved latest release. */ + async getMod( + gameId: string, + key: string, + ): Promise<{ + mod: ModRecord; + releases: Array; + latest: (ModReleaseRecord & { parsed: ModManifest }) | null; + }> { + const mod = await this.deps.prisma.mod.findUnique({ + where: { gameId_key: { gameId, key } }, + }); + if (!mod) { + throw createError({ statusCode: 404, statusMessage: "Unknown mod" }); + } + const releases = await this.deps.prisma.modRelease.findMany({ + where: { modId: mod.id }, + orderBy: { createdAt: "desc" }, + }); + const withParsed = releases.map((release) => ({ + ...release, + parsed: parseModManifest(JSON.parse(release.manifest)), + })); + const latestRecord = latestRelease(releases); + const latest = latestRecord + ? (withParsed.find((r) => r.id === latestRecord.id) ?? null) + : null; + return { mod, releases: withParsed, latest }; + } + + /** Subscribe a user to a mod (optionally pinned to a version). */ + async subscribe( + userId: string, + gameId: string, + key: string, + pinnedVersion?: string, + ): Promise { + const mod = await this.deps.prisma.mod.findUnique({ + where: { gameId_key: { gameId, key } }, + }); + if (!mod) { + throw createError({ statusCode: 404, statusMessage: "Unknown mod" }); + } + const existing = await this.deps.prisma.modSubscription.findUnique({ + where: { userId_modId: { userId, modId: mod.id } }, + }); + if (existing) { + return this.deps.prisma.modSubscription.update({ + where: { userId_modId: { userId, modId: mod.id } }, + data: { pinnedVersion: pinnedVersion ?? null }, + }); + } + return this.deps.prisma.modSubscription.create({ + data: { + userId, + modId: mod.id, + ...(pinnedVersion ? { pinnedVersion } : {}), + }, + }); + } + + /** Unsubscribe a user; returns whether a subscription existed. */ + async unsubscribe( + userId: string, + gameId: string, + key: string, + ): Promise { + const mod = await this.deps.prisma.mod.findUnique({ + where: { gameId_key: { gameId, key } }, + }); + if (!mod) return false; + const existing = await this.deps.prisma.modSubscription.findUnique({ + where: { userId_modId: { userId, modId: mod.id } }, + }); + if (!existing) return false; + await this.deps.prisma.modSubscription.delete({ + where: { userId_modId: { userId, modId: mod.id } }, + }); + return true; + } + + async listSubscriptions(userId: string): Promise { + return this.deps.prisma.modSubscription.findMany({ where: { userId } }); + } +} diff --git a/server/server/internal/workshop/manifest.ts b/server/server/internal/workshop/manifest.ts new file mode 100644 index 000000000..c3da1f2c9 --- /dev/null +++ b/server/server/internal/workshop/manifest.ts @@ -0,0 +1,128 @@ +import { createError } from "h3"; + +/** + * `mod.json` manifest for The Drop Workshop (#20). + * + * A manifest is the portable description of a mod: its identity, the game + * release it targets, its dependency graph, and the files it ships. It is + * validated before it is stored so a malformed or hostile manifest can never + * reach the desktop mod manager (path traversal, absolute paths, unknown + * shapes). + */ + +export interface ModDependency { + id: string; + version?: string; +} + +export interface ModManifest { + id: string; + name: string; + version: string; + author?: string; + description?: string; + game?: { + appId?: string; + version?: string; + }; + dependencies: ModDependency[]; + files: string[]; +} + +/** Loose semver: MAJOR.MINOR.PATCH with optional prerelease/build metadata. */ +const VERSION_RE = /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/; + +function fail(message: string): never { + throw createError({ statusCode: 400, statusMessage: message }); +} + +function requireString(value: unknown, field: string): string { + if (typeof value !== "string" || value.trim().length === 0) { + fail(`${field} is required`); + } + return value.trim(); +} + +function optionalString(value: unknown): string | undefined { + return typeof value === "string" && value.trim().length > 0 + ? value.trim() + : undefined; +} + +/** Reject absolute paths and `..` traversal in bundled mod files. */ +export function isSafeModPath(path: string): boolean { + if (path.startsWith("/") || path.startsWith("\\")) return false; + if (/^[a-zA-Z]:[\\/]/.test(path)) return false; + return !path.split(/[\\/]/).includes(".."); +} + +function parseDependencies(value: unknown): ModDependency[] { + if (value === undefined || value === null) return []; + if (!Array.isArray(value)) fail("dependencies must be an array"); + return value.map((entry) => { + if (typeof entry === "string") { + return { id: requireString(entry, "dependency id") }; + } + if (!entry || typeof entry !== "object" || Array.isArray(entry)) { + fail("each dependency must be a string or an object"); + } + const record = entry as Record; + const id = requireString(record.id, "dependency id"); + const version = optionalString(record.version); + return version ? { id, version } : { id }; + }); +} + +function parseFiles(value: unknown): string[] { + if (value === undefined || value === null) return []; + if (!Array.isArray(value)) fail("files must be an array"); + return value.map((entry) => { + const path = requireString(entry, "file path"); + if (!isSafeModPath(path)) { + fail(`unsafe file path '${path}'`); + } + return path; + }); +} + +function parseGame(value: unknown): ModManifest["game"] { + if (value === undefined || value === null) return undefined; + if (typeof value !== "object" || Array.isArray(value)) { + fail("game must be an object"); + } + const record = value as Record; + const appId = optionalString(record.appId); + const version = optionalString(record.version); + if (!appId && !version) return undefined; + return { ...(appId ? { appId } : {}), ...(version ? { version } : {}) }; +} + +/** Parse and validate a `mod.json` payload. */ +export function parseModManifest(input: unknown): ModManifest { + if (!input || typeof input !== "object" || Array.isArray(input)) { + fail("manifest must be an object"); + } + const raw = input as Record; + const id = requireString(raw.id, "id"); + const name = requireString(raw.name, "name"); + const version = requireString(raw.version, "version"); + if (!VERSION_RE.test(version)) { + fail("version must be semver (MAJOR.MINOR.PATCH)"); + } + + const manifest: ModManifest = { + id, + name, + version, + dependencies: parseDependencies(raw.dependencies), + files: parseFiles(raw.files), + }; + const author = optionalString(raw.author); + if (author) manifest.author = author; + const description = optionalString(raw.description); + if (description) manifest.description = description; + const game = parseGame(raw.game); + if (game) manifest.game = game; + + return manifest; +} From c6b92063feb7e7521a26ffb22053d6ce1c88e3e4 Mon Sep 17 00:00:00 2001 From: John Smith Date: Mon, 14 Sep 2026 12:41:28 -0400 Subject: [PATCH 213/243] feat(desktop): stage shader caches before launch and document the pack format Phase 1 / #17 remaining shader pre-caching work: - shader_cache::stage_shader_caches restores packaged *.dxvk-cache files into the install directory before launch, preserving a locally warmed cache unless overwrite is requested. - docs/implementation/shader-cache-format.md defines the manifest/pack format shared with the drop-gamebox community index, including SHA-256 verification and trust notes. - 1 new test (cache-only filtering, preserve, overwrite). --- desktop/src-tauri/shader_cache/src/lib.rs | 82 ++++++++++++++++++++++ docs/implementation/roadmap-status.md | 12 ++-- docs/implementation/shader-cache-format.md | 64 +++++++++++++++++ 3 files changed, 152 insertions(+), 6 deletions(-) create mode 100644 docs/implementation/shader-cache-format.md diff --git a/desktop/src-tauri/shader_cache/src/lib.rs b/desktop/src-tauri/shader_cache/src/lib.rs index 4f51f5db1..827880133 100644 --- a/desktop/src-tauri/shader_cache/src/lib.rs +++ b/desktop/src-tauri/shader_cache/src/lib.rs @@ -77,6 +77,38 @@ pub fn collect_shader_caches( Ok(copied) } +/// Restore cached DXVK/VKD3D state files into the install directory before a +/// game launches (the "sync" half of community shader pre-caching). +/// +/// Non-cache sources are ignored. Existing files are preserved unless +/// `overwrite` is set, so a locally warmed cache is never replaced by an older +/// download during a normal launch. +pub fn stage_shader_caches( + sources: &[PathBuf], + install_dir: &Path, + overwrite: bool, +) -> io::Result> { + fs::create_dir_all(install_dir)?; + + let mut staged = Vec::new(); + for source in sources { + if !is_shader_cache_file(source) { + continue; + } + let Some(name) = source.file_name() else { + continue; + }; + let target = install_dir.join(name); + if target.exists() && !overwrite { + continue; + } + fs::copy(source, &target)?; + staged.push(target); + } + + Ok(staged) +} + #[cfg(test)] mod tests { use super::*; @@ -130,4 +162,54 @@ mod tests { assert!(names.contains(&"game.dxvk-cache".to_string())); assert!(names.contains(&"1-game.dxvk-cache".to_string())); } + + #[test] + fn stages_only_cache_files_and_preserves_local_warm_cache() { + let source = tempdir().unwrap(); + let install = tempdir().unwrap(); + fs::write(source.path().join("game.dxvk-cache"), b"downloaded").unwrap(); + fs::write(source.path().join("readme.txt"), b"ignore").unwrap(); + + let staged = stage_shader_caches( + &[ + source.path().join("game.dxvk-cache"), + source.path().join("readme.txt"), + ], + install.path(), + false, + ) + .unwrap(); + assert_eq!(staged.len(), 1); + assert_eq!( + fs::read(install.path().join("game.dxvk-cache")).unwrap(), + b"downloaded" + ); + + // A locally warmed cache is preserved without overwrite... + fs::write(install.path().join("game.dxvk-cache"), b"local-warm").unwrap(); + let preserved = stage_shader_caches( + &[source.path().join("game.dxvk-cache")], + install.path(), + false, + ) + .unwrap(); + assert!(preserved.is_empty()); + assert_eq!( + fs::read(install.path().join("game.dxvk-cache")).unwrap(), + b"local-warm" + ); + + // ...and replaced when overwrite is requested. + let replaced = stage_shader_caches( + &[source.path().join("game.dxvk-cache")], + install.path(), + true, + ) + .unwrap(); + assert_eq!(replaced.len(), 1); + assert_eq!( + fs::read(install.path().join("game.dxvk-cache")).unwrap(), + b"downloaded" + ); + } } diff --git a/docs/implementation/roadmap-status.md b/docs/implementation/roadmap-status.md index 318e7ca20..06d6015f4 100644 --- a/docs/implementation/roadmap-status.md +++ b/docs/implementation/roadmap-status.md @@ -31,12 +31,12 @@ tests and a pushed commit; `🟡` usable contract/reference implementation; ## Tranches 4–7 -| Tranche | Status | Notes | -| :--------------------------------- | :----- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| #17 Content delivery | ✅ | `downpour push` CDC chunker + zstd + memory/S3 upload with binary delta reuse (`--base`) and `--branch`/`--depot` manifest metadata; torrential LAN registry, SSDP discovery, HTTP chunk fetcher with peer-chunk SHA-256 verification (`verify_chunk_sha256` / `fetch_verified_from_best_peer`); shader-cache discovery + `drop-gamebox` index; PCGamingWiki externalized. QUIC is contract-only and deferred: `torrential/src/lan/mod.rs` defines the `ChunkFetcher` trait, HTTP is the implemented transport. | -| #18 Handheld/Input/Overlay | 🟡 | `input` crate (mapping + `NullBackend` availability reporting) + Big Picture page + on-screen keyboard ✅; native uinput/ViGEm backends are **not implemented** (the `BackendKind` variants are API surface only) ⛔; Vulkan/DXGI overlay ⛔ (needs the Vulkan layer toolchain). | -| #8 Achievements | ✅ | Prisma models + migration, unlock API, `drop:achievement:unlock` bus; GSE `achievements.json` parser + addon bridge; RetroAchievements mapping/emission; desktop unlock toasts mounted. | -| #20/#21 Social/Federation/Commerce | ✅ | verified reviews + community page; Ed25519 receipts + webhook dispatch; federation presence; ODP signed catalog syndication; store-scanner aggregation. Store/payment provider API keys are operator-supplied (reported). | +| Tranche | Status | Notes | +| :--------------------------------- | :----- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| #17 Content delivery | ✅ | `downpour push` CDC chunker + zstd + memory/S3 upload with binary delta reuse (`--base`) and `--branch`/`--depot` manifest metadata; torrential LAN registry, SSDP discovery, HTTP chunk fetcher with peer-chunk SHA-256 verification (`verify_chunk_sha256` / `fetch_verified_from_best_peer`); shader-cache discovery, packaging format doc (`docs/implementation/shader-cache-format.md`) and `stage_shader_caches` pre-launch staging + `drop-gamebox` index; PCGamingWiki externalized. QUIC is contract-only and deferred: `torrential/src/lan/mod.rs` defines the `ChunkFetcher` trait, HTTP is the implemented transport. | +| #18 Handheld/Input/Overlay | 🟡 | `input` crate (mapping + `NullBackend` availability reporting) + Big Picture page + on-screen keyboard ✅; native uinput/ViGEm backends are **not implemented** (the `BackendKind` variants are API surface only) ⛔; Vulkan/DXGI overlay ⛔ (needs the Vulkan layer toolchain). | +| #8 Achievements | ✅ | Prisma models + migration, unlock API, `drop:achievement:unlock` bus; GSE `achievements.json` parser + addon bridge; RetroAchievements mapping/emission; desktop unlock toasts mounted. | +| #20/#21 Social/Federation/Commerce | ✅ | verified reviews + community page; Ed25519 receipts + webhook dispatch; federation presence; ODP signed catalog syndication; store-scanner aggregation. Store/payment provider API keys are operator-supplied (reported). | ## Hard blockers (reported, not stubbed) diff --git a/docs/implementation/shader-cache-format.md b/docs/implementation/shader-cache-format.md new file mode 100644 index 000000000..805f8591f --- /dev/null +++ b/docs/implementation/shader-cache-format.md @@ -0,0 +1,64 @@ +# Shader-cache packaging format (Phase 1, #17) + +DXVK/VKD3D compile pipeline state into `*.dxvk-cache` files. Drop pre-caches +them so a game does not stutter while shaders compile on first play. This +document fixes the packaging format shared by the client collector +(`desktop/src-tauri/shader_cache`) and the community index +(`drop-gamebox` `/shader-cache/*`). + +## Package + +A shader-cache package is a directory (or a deterministic archive of that +directory) containing: + +``` +manifest.json +shaders/.dxvk-cache +shaders/.vkd3d-cache +``` + +- Only `*.dxvk-cache` and `*.vkd3d-cache` files are included. Discovery walks + the install directory and, on Linux, the Proton/UMU prefix `drive_c` tree + (`shader_cache::find_shader_caches`). +- File names are flattened with a numeric prefix on collision + (`shader_cache::collect_shader_caches`), so two caches with the same base + name do not overwrite each other. + +## `manifest.json` + +```json +{ + "createdAt": 1760000000000, + "driver": "amd", + "dxvkVersion": "2.4.1", + "entries": [{ "name": "game.dxvk-cache", "sha256": "…", "sizeBytes": 1234 }], + "executable": "game.exe", + "formatVersion": 1, + "gameId": "uuid" +} +``` + +- `sha256` is the lowercase hex digest of the cache file's bytes and is the + identity used by the index (`drop-gamebox` stores + `shader-cache:` → per-driver `{ sha256, sizeBytes, dxvkVersion }`). +- `driver` is a coarse bucket (`amd`, `nvidia`, `intel`) because state caches + are not portable across vendors. +- `executable` is the game binary the cache was warmed against; Steam Deck and + desktop caches are often interchangeable for the same build. + +## Client behavior + +1. **Collect (contribute):** discover caches, copy them into a package + directory, hash each file, and upload the manifest to the community index. +2. **Stage (restore before launch):** before spawning the game, copy the + newest matching package's caches into the install directory with + `shader_cache::stage_shader_caches`. A locally warmed cache is preserved by + default (`overwrite = false`); an explicit user action may force + replacement. + +## Trust + +Shader caches are untrusted input from the community index. The client verifies +each file's SHA-256 against the manifest before staging, and the index +(`drop-gamebox`) only ranks entries by verified size/recency. Caches never +execute; they are data files consumed by the graphics driver. From 664b7d12ec612284d57e9f8897bc65adb80937d1 Mon Sep 17 00:00:00 2001 From: John Smith Date: Mon, 14 Sep 2026 12:45:07 -0400 Subject: [PATCH 214/243] feat(server): validate admin depot endpoints before registration Advances Heretek-Games/drop-seedbox#3 (remote depot registration): - POST /api/v1/admin/depot/validate probes an admin-supplied depot endpoint's speedtest object with a short timeout. - SSRF-conscious: only http/https URLs are accepted and only the fixed speedtest path is requested; the response reports reachability, status and latency instead of throwing. --- .../api/v1/admin/depot/validate.post.ts | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 server/server/api/v1/admin/depot/validate.post.ts diff --git a/server/server/api/v1/admin/depot/validate.post.ts b/server/server/api/v1/admin/depot/validate.post.ts new file mode 100644 index 000000000..5cc19cc56 --- /dev/null +++ b/server/server/api/v1/admin/depot/validate.post.ts @@ -0,0 +1,64 @@ +import { type } from "arktype"; +import { readDropValidatedBody, throwingArktype } from "~/server/arktype"; +import aclManager from "~/server/internal/acls"; + +const ValidateDepot = type({ + endpoint: "string", +}).configure(throwingArktype); + +const PROBE_TIMEOUT_MS = 5_000; + +/** + * Probes an admin-supplied depot endpoint before it is registered. + * + * The endpoint is treated as an SSRF-sensitive value: only `http`/`https` URLs + * are allowed, the request carries a short timeout, and only the depot's + * `speedtest` object is fetched — never an operator-provided path. + */ +export default defineEventHandler(async (h3) => { + const allowed = await aclManager.allowSystemACL(h3, ["depot:read"]); + if (!allowed) throw createError({ statusCode: 403 }); + + const body = await readDropValidatedBody(h3, ValidateDepot); + + let parsed: URL; + try { + parsed = new URL(body.endpoint); + } catch { + throw createError({ + statusCode: 400, + message: "endpoint is not a valid URL", + }); + } + if (parsed.protocol !== "http:" && parsed.protocol !== "https:") { + throw createError({ + statusCode: 400, + message: "endpoint must use http or https", + }); + } + + const base = body.endpoint.endsWith("/") + ? body.endpoint + : `${body.endpoint}/`; + const startedAt = Date.now(); + try { + const response = await fetch(`${base}speedtest`, { + method: "GET", + signal: AbortSignal.timeout(PROBE_TIMEOUT_MS), + }); + return { + reachable: response.ok, + status: response.status, + latencyMs: Date.now() - startedAt, + error: response.ok + ? undefined + : `depot responded with ${response.status}`, + }; + } catch (error) { + return { + reachable: false, + latencyMs: Date.now() - startedAt, + error: error instanceof Error ? error.message : "unknown error", + }; + } +}); From a58e6bfb77b748b85bf6e0dc10eae260c7657a3f Mon Sep 17 00:00:00 2001 From: John Smith Date: Mon, 14 Sep 2026 12:45:39 -0400 Subject: [PATCH 215/243] docs(roadmap): record workshop, leaderboards, federation and mirror progress --- docs/implementation/roadmap-status.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/implementation/roadmap-status.md b/docs/implementation/roadmap-status.md index 06d6015f4..627d5e016 100644 --- a/docs/implementation/roadmap-status.md +++ b/docs/implementation/roadmap-status.md @@ -35,8 +35,8 @@ tests and a pushed commit; `🟡` usable contract/reference implementation; | :--------------------------------- | :----- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | #17 Content delivery | ✅ | `downpour push` CDC chunker + zstd + memory/S3 upload with binary delta reuse (`--base`) and `--branch`/`--depot` manifest metadata; torrential LAN registry, SSDP discovery, HTTP chunk fetcher with peer-chunk SHA-256 verification (`verify_chunk_sha256` / `fetch_verified_from_best_peer`); shader-cache discovery, packaging format doc (`docs/implementation/shader-cache-format.md`) and `stage_shader_caches` pre-launch staging + `drop-gamebox` index; PCGamingWiki externalized. QUIC is contract-only and deferred: `torrential/src/lan/mod.rs` defines the `ChunkFetcher` trait, HTTP is the implemented transport. | | #18 Handheld/Input/Overlay | 🟡 | `input` crate (mapping + `NullBackend` availability reporting) + Big Picture page + on-screen keyboard ✅; native uinput/ViGEm backends are **not implemented** (the `BackendKind` variants are API surface only) ⛔; Vulkan/DXGI overlay ⛔ (needs the Vulkan layer toolchain). | -| #8 Achievements | ✅ | Prisma models + migration, unlock API, `drop:achievement:unlock` bus; GSE `achievements.json` parser + addon bridge; RetroAchievements mapping/emission; desktop unlock toasts mounted. | -| #20/#21 Social/Federation/Commerce | ✅ | verified reviews + community page; Ed25519 receipts + webhook dispatch; federation presence; ODP signed catalog syndication; store-scanner aggregation. Store/payment provider API keys are operator-supplied (reported). | +| #8 Achievements | ✅ | Prisma models + migration, unlock API, `drop:achievement:unlock` bus; leaderboard API (`submitScore` best-score retention, `listLeaderboard`) + `GET/POST /api/v1/client/leaderboards/:gameid/:key`; GSE `achievements.json` parser + addon bridge; RetroAchievements mapping/emission; desktop unlock toasts mounted. | +| #20/#21 Social/Federation/Commerce | ✅ | verified reviews + community page; Ed25519 receipts + webhook dispatch; federation presence + moderation (block/revoke/rate-limit) and provenance-aware ODP discovery search; Drop Workshop `mod.json` registry + subscriptions; GameBox verifiable index snapshots/mirrors; depot endpoint validation; store-scanner aggregation. Store/payment provider API keys are operator-supplied (reported). | ## Hard blockers (reported, not stubbed) From 455152740d119b4c997ad23a1d329ab9b4d25610 Mon Sep 17 00:00:00 2001 From: John Smith Date: Mon, 14 Sep 2026 12:47:45 -0400 Subject: [PATCH 216/243] feat(server): implement the Dropworks native SDK REST contract Phase 3 / #19: the dropworks-sdk reference clients expect a server API that did not exist. - DropworksManager exchanges an appId + Drop API token for a session and unlocks achievements, always deriving the user from the token. - POST /api/v1/dropworks/session -> { userId, gameId, appId }. - POST /api/v1/dropworks/achievement -> unlocks by stable key; a client-supplied userId must match the authenticated session (no cross-user spoofing). - Token from body authToken or Authorization: Bearer; appId resolves by Drop game id or external metadata id. - 3 manager tests (session resolution, rejection paths, unlock auth). --- .../api/v1/dropworks/achievement.post.ts | 34 ++++++ .../server/api/v1/dropworks/session.post.ts | 32 +++++ .../dropworks/__tests__/dropworks.test.ts | 83 +++++++++++++ server/server/internal/dropworks/index.ts | 30 +++++ server/server/internal/dropworks/manager.ts | 112 ++++++++++++++++++ 5 files changed, 291 insertions(+) create mode 100644 server/server/api/v1/dropworks/achievement.post.ts create mode 100644 server/server/api/v1/dropworks/session.post.ts create mode 100644 server/server/internal/dropworks/__tests__/dropworks.test.ts create mode 100644 server/server/internal/dropworks/index.ts create mode 100644 server/server/internal/dropworks/manager.ts diff --git a/server/server/api/v1/dropworks/achievement.post.ts b/server/server/api/v1/dropworks/achievement.post.ts new file mode 100644 index 000000000..19717b7d8 --- /dev/null +++ b/server/server/api/v1/dropworks/achievement.post.ts @@ -0,0 +1,34 @@ +import type { H3Event } from "h3"; +import dropworksManager from "~/server/internal/dropworks"; + +interface AchievementBody { + appId?: string; + userId?: string; + achievementId?: string; + authToken?: string; +} + +function bearerToken(h3: H3Event): string | undefined { + const header = getHeader(h3, "Authorization"); + if (!header) return undefined; + const [type, token] = header.split(" "); + return type === "Bearer" && token ? token : undefined; +} + +export default defineEventHandler(async (h3) => { + const body = ((await readBody(h3)) ?? {}) as AchievementBody; + const token = body.authToken ?? bearerToken(h3); + if (!token) { + throw createError({ + statusCode: 401, + statusMessage: "A Dropworks auth token is required", + }); + } + + return await dropworksManager.unlockAchievement( + token, + body.appId ?? "", + body.achievementId ?? "", + body.userId, + ); +}); diff --git a/server/server/api/v1/dropworks/session.post.ts b/server/server/api/v1/dropworks/session.post.ts new file mode 100644 index 000000000..0b16bf875 --- /dev/null +++ b/server/server/api/v1/dropworks/session.post.ts @@ -0,0 +1,32 @@ +import type { H3Event } from "h3"; +import dropworksManager from "~/server/internal/dropworks"; + +interface SessionBody { + appId?: string; + authToken?: string; +} + +function bearerToken(h3: H3Event): string | undefined { + const header = getHeader(h3, "Authorization"); + if (!header) return undefined; + const [type, token] = header.split(" "); + return type === "Bearer" && token ? token : undefined; +} + +export default defineEventHandler(async (h3) => { + const body = ((await readBody(h3)) ?? {}) as SessionBody; + const token = body.authToken ?? bearerToken(h3); + if (!body.appId || !token) { + throw createError({ + statusCode: 400, + statusMessage: "appId and authToken are required", + }); + } + + const session = await dropworksManager.openSession(body.appId, token); + return { + userId: session.userId, + gameId: session.gameId, + appId: session.appId, + }; +}); diff --git a/server/server/internal/dropworks/__tests__/dropworks.test.ts b/server/server/internal/dropworks/__tests__/dropworks.test.ts new file mode 100644 index 000000000..8b3fa7dee --- /dev/null +++ b/server/server/internal/dropworks/__tests__/dropworks.test.ts @@ -0,0 +1,83 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { DropworksManager, type DropworksDeps } from "../manager"; + +function createHarness(overrides: Partial = {}) { + const deps: DropworksDeps = { + resolveUserByToken: async (token) => + token === "valid" ? "user-1" : undefined, + findGameId: async (appId) => + appId === "game-1" || appId === "12345" ? "game-id-1" : undefined, + unlockAchievement: async () => ({ alreadyUnlocked: false }), + ...overrides, + }; + return new DropworksManager(deps); +} + +test("openSession resolves the token and app id", async () => { + const manager = createHarness(); + + const session = await manager.openSession("game-1", "valid"); + assert.deepEqual(session, { + userId: "user-1", + gameId: "game-id-1", + appId: "game-1", + }); + + // An external metadata app id resolves to the same Drop game. + const viaMetadata = await manager.openSession("12345", "valid"); + assert.equal(viaMetadata.gameId, "game-id-1"); +}); + +test("openSession rejects missing input, bad token and unknown app", async () => { + const manager = createHarness(); + + await assert.rejects( + () => manager.openSession("", "valid"), + /appId and authToken/, + ); + await assert.rejects( + () => manager.openSession("game-1", "nope"), + /Invalid Dropworks auth token/, + ); + await assert.rejects( + () => manager.openSession("missing", "valid"), + /Unknown appId/, + ); +}); + +test("unlockAchievement authenticates and rejects userId spoofing", async () => { + let seen: { userId: string; gameId: string; key: string } | null = null; + const manager = createHarness({ + unlockAchievement: async (userId, gameId, key) => { + seen = { userId, gameId, key }; + return { alreadyUnlocked: true }; + }, + }); + + const result = await manager.unlockAchievement( + "valid", + "game-1", + "beat-boss", + "user-1", + ); + assert.deepEqual(result, { unlocked: true, alreadyUnlocked: true }); + assert.deepEqual(seen, { + userId: "user-1", + gameId: "game-id-1", + key: "beat-boss", + }); + + await assert.rejects( + () => manager.unlockAchievement("valid", "game-1", "beat-boss", "attacker"), + /does not match/, + ); + await assert.rejects( + () => manager.unlockAchievement("nope", "game-1", "beat-boss"), + /Invalid Dropworks auth token/, + ); + await assert.rejects( + () => manager.unlockAchievement("valid", "", ""), + /appId and achievementId/, + ); +}); diff --git a/server/server/internal/dropworks/index.ts b/server/server/internal/dropworks/index.ts new file mode 100644 index 000000000..95df889d1 --- /dev/null +++ b/server/server/internal/dropworks/index.ts @@ -0,0 +1,30 @@ +import prisma from "../db/database"; +import achievementManager from "../achievements"; +import { APITokenMode } from "~/prisma/client/enums"; +import { DropworksManager, type DropworksDeps } from "./manager"; + +const deps: DropworksDeps = { + resolveUserByToken: async (token) => { + const record = await prisma.aPIToken.findUnique({ + where: { + token, + mode: { in: [APITokenMode.User, APITokenMode.Client] }, + }, + }); + return record?.userId ?? undefined; + }, + findGameId: async (appId) => { + const game = await prisma.game.findFirst({ + where: { OR: [{ id: appId }, { metadataId: appId }] }, + select: { id: true }, + }); + return game?.id; + }, + unlockAchievement: (userId, gameId, key) => + achievementManager.unlock(userId, gameId, key), +}; + +export const dropworksManager = new DropworksManager(deps); +export { DropworksManager }; +export * from "./manager"; +export default dropworksManager; diff --git a/server/server/internal/dropworks/manager.ts b/server/server/internal/dropworks/manager.ts new file mode 100644 index 000000000..4f41810b2 --- /dev/null +++ b/server/server/internal/dropworks/manager.ts @@ -0,0 +1,112 @@ +import { createError } from "h3"; + +/** + * Dropworks game SDK service (#19). + * + * Native games authenticate with a Drop API token (User or Client mode) and an + * `appId` that resolves to a Drop game. The reference TypeScript and Rust + * clients in `Heretek-Games/dropworks-sdk` speak this contract: + * + * POST /api/v1/dropworks/session -> { userId } + * POST /api/v1/dropworks/achievement -> unlock an achievement + * + * The authenticated user is always derived from the token; a client-supplied + * `userId` is only accepted when it matches, so one game session cannot unlock + * achievements on another user's behalf. + */ + +export interface DropworksSession { + userId: string; + gameId: string; + appId: string; +} + +export interface UnlockResult { + unlocked: boolean; + alreadyUnlocked: boolean; +} + +/** Dependency surface kept structural so the service is unit-testable. */ +export interface DropworksDeps { + /** Resolve a User/Client API token to its owning user id. */ + resolveUserByToken(token: string): Promise; + /** Resolve a client `appId` (Drop game id or external metadata id). */ + findGameId(appId: string): Promise; + /** Unlock an achievement by its stable key. */ + unlockAchievement( + userId: string, + gameId: string, + key: string, + ): Promise<{ alreadyUnlocked: boolean }>; +} + +export class DropworksManager { + constructor(private readonly deps: DropworksDeps) {} + + /** Exchange an app id + API token for a game session. */ + async openSession(appId: string, token: string): Promise { + if (!appId || !token) { + throw createError({ + statusCode: 400, + statusMessage: "appId and authToken are required", + }); + } + const userId = await this.deps.resolveUserByToken(token); + if (!userId) { + throw createError({ + statusCode: 401, + statusMessage: "Invalid Dropworks auth token", + }); + } + const gameId = await this.deps.findGameId(appId); + if (!gameId) { + throw createError({ + statusCode: 404, + statusMessage: "Unknown appId", + }); + } + return { userId, gameId, appId }; + } + + /** + * Unlock an achievement for the authenticated user. `claimUserId`, when + * provided, must match the authenticated user. + */ + async unlockAchievement( + token: string, + appId: string, + achievementId: string, + claimUserId?: string, + ): Promise { + if (!appId || !achievementId) { + throw createError({ + statusCode: 400, + statusMessage: "appId and achievementId are required", + }); + } + const userId = await this.deps.resolveUserByToken(token); + if (!userId) { + throw createError({ + statusCode: 401, + statusMessage: "Invalid Dropworks auth token", + }); + } + if (claimUserId && claimUserId !== userId) { + throw createError({ + statusCode: 403, + statusMessage: "userId does not match the authenticated session", + }); + } + const gameId = await this.deps.findGameId(appId); + if (!gameId) { + throw createError({ statusCode: 404, statusMessage: "Unknown appId" }); + } + + const { alreadyUnlocked } = await this.deps.unlockAchievement( + userId, + gameId, + achievementId, + ); + return { unlocked: true, alreadyUnlocked }; + } +} From 9fca2135cfddada0e5600c2f9d1dcf68f9296996 Mon Sep 17 00:00:00 2001 From: John Smith Date: Mon, 14 Sep 2026 12:52:20 -0400 Subject: [PATCH 217/243] feat(server): add per-game discussion forums Phase 4 / #20 community hubs (forums were not started): - Prisma ForumThread / ForumPost models + migration and Game/User back-relations. - ForumManager with create/list/get/reply (locked threads reject) and delete, plus title/body length caps. - Public GET routes for threads and a thread with its replies; client routes for creating, replying and deleting (author or admin only). - 4 manager tests (validation, caps, locking, deletion). --- .../20260918000000_add_forums/migration.sql | 46 +++++ server/prisma/models/community.prisma | 39 ++++ server/prisma/models/content.prisma | 1 + server/prisma/models/user.prisma | 2 + .../community/[gameid]/threads/index.post.ts | 31 +++ .../community/threads/[threadid].delete.ts | 25 +++ .../threads/[threadid]/posts.post.ts | 24 +++ .../community/[gameid]/threads/index.get.ts | 10 + .../v1/community/threads/[threadid].get.ts | 15 ++ .../community/__tests__/forum.test.ts | 147 ++++++++++++++ server/server/internal/community/forum.ts | 181 ++++++++++++++++++ server/server/internal/community/index.ts | 12 ++ 12 files changed, 533 insertions(+) create mode 100644 server/prisma/migrations/20260918000000_add_forums/migration.sql create mode 100644 server/server/api/v1/client/community/[gameid]/threads/index.post.ts create mode 100644 server/server/api/v1/client/community/threads/[threadid].delete.ts create mode 100644 server/server/api/v1/client/community/threads/[threadid]/posts.post.ts create mode 100644 server/server/api/v1/community/[gameid]/threads/index.get.ts create mode 100644 server/server/api/v1/community/threads/[threadid].get.ts create mode 100644 server/server/internal/community/__tests__/forum.test.ts create mode 100644 server/server/internal/community/forum.ts diff --git a/server/prisma/migrations/20260918000000_add_forums/migration.sql b/server/prisma/migrations/20260918000000_add_forums/migration.sql new file mode 100644 index 000000000..be87ea36a --- /dev/null +++ b/server/prisma/migrations/20260918000000_add_forums/migration.sql @@ -0,0 +1,46 @@ +-- CreateTable +CREATE TABLE "ForumThread" ( + "id" TEXT NOT NULL, + "gameId" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "title" TEXT NOT NULL, + "body" TEXT NOT NULL, + "locked" BOOLEAN NOT NULL DEFAULT false, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "ForumThread_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "ForumPost" ( + "id" TEXT NOT NULL, + "threadId" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "body" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "ForumPost_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE INDEX "ForumThread_gameId_idx" ON "ForumThread"("gameId"); + +-- CreateIndex +CREATE INDEX "ForumThread_gameId_updatedAt_idx" ON "ForumThread"("gameId", "updatedAt"); + +-- CreateIndex +CREATE INDEX "ForumPost_threadId_createdAt_idx" ON "ForumPost"("threadId", "createdAt"); + +-- AddForeignKey +ALTER TABLE "ForumThread" ADD CONSTRAINT "ForumThread_gameId_fkey" FOREIGN KEY ("gameId") REFERENCES "Game"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ForumThread" ADD CONSTRAINT "ForumThread_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ForumPost" ADD CONSTRAINT "ForumPost_threadId_fkey" FOREIGN KEY ("threadId") REFERENCES "ForumThread"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ForumPost" ADD CONSTRAINT "ForumPost_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/server/prisma/models/community.prisma b/server/prisma/models/community.prisma index fd330d0b1..a70be4266 100644 --- a/server/prisma/models/community.prisma +++ b/server/prisma/models/community.prisma @@ -18,3 +18,42 @@ model GameReview { @@unique([gameId, userId]) @@index([gameId]) } + +// Per-game discussion forums (#20 community hubs). + +model ForumThread { + id String @id @default(uuid()) + + gameId String + game Game @relation(fields: [gameId], references: [id], onDelete: Cascade) + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + title String + body String + locked Boolean @default(false) + + createdAt DateTime @default(now()) + updatedAt DateTime @default(now()) @updatedAt + + posts ForumPost[] + + @@index([gameId]) + @@index([gameId, updatedAt]) +} + +model ForumPost { + id String @id @default(uuid()) + + threadId String + thread ForumThread @relation(fields: [threadId], references: [id], onDelete: Cascade) + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + body String + + createdAt DateTime @default(now()) + updatedAt DateTime @default(now()) @updatedAt + + @@index([threadId, createdAt]) +} diff --git a/server/prisma/models/content.prisma b/server/prisma/models/content.prisma index 02f93f7f2..dc9b35a16 100644 --- a/server/prisma/models/content.prisma +++ b/server/prisma/models/content.prisma @@ -64,6 +64,7 @@ model Game { leaderboards Leaderboard[] reviews GameReview[] mods Mod[] + forumThreads ForumThread[] developers Company[] @relation(name: "developers") publishers Company[] @relation(name: "publishers") diff --git a/server/prisma/models/user.prisma b/server/prisma/models/user.prisma index 1136cbe6d..e335c81d5 100644 --- a/server/prisma/models/user.prisma +++ b/server/prisma/models/user.prisma @@ -27,6 +27,8 @@ model User { leaderboardEntries LeaderboardEntry[] reviews GameReview[] modSubscriptions ModSubscription[] + forumThreads ForumThread[] + forumPosts ForumPost[] groups UserGroup[] } diff --git a/server/server/api/v1/client/community/[gameid]/threads/index.post.ts b/server/server/api/v1/client/community/[gameid]/threads/index.post.ts new file mode 100644 index 000000000..6a3f42005 --- /dev/null +++ b/server/server/api/v1/client/community/[gameid]/threads/index.post.ts @@ -0,0 +1,31 @@ +import { defineClientEventHandler } from "~/server/internal/clients/event-handler"; +import { forumManager } from "~/server/internal/community"; + +interface CreateThreadBody { + title?: string; + body?: string; +} + +export default defineClientEventHandler(async (h3, { fetchUser }) => { + const user = await fetchUser(); + const gameId = getRouterParam(h3, "gameid"); + if (!gameId) + throw createError({ statusCode: 400, statusMessage: "gameId is required" }); + + const payload = ((await readBody(h3)) ?? + {}) as CreateThreadBody; + if (typeof payload.title !== "string" || typeof payload.body !== "string") { + throw createError({ + statusCode: 400, + statusMessage: "title and body are required", + }); + } + + const thread = await forumManager.createThread( + user.id, + gameId, + payload.title, + payload.body, + ); + return { success: true, thread }; +}); diff --git a/server/server/api/v1/client/community/threads/[threadid].delete.ts b/server/server/api/v1/client/community/threads/[threadid].delete.ts new file mode 100644 index 000000000..e53d9800c --- /dev/null +++ b/server/server/api/v1/client/community/threads/[threadid].delete.ts @@ -0,0 +1,25 @@ +import { defineClientEventHandler } from "~/server/internal/clients/event-handler"; +import { forumManager } from "~/server/internal/community"; + +export default defineClientEventHandler(async (h3, { fetchUser }) => { + const user = await fetchUser(); + const threadId = getRouterParam(h3, "threadid"); + if (!threadId) + throw createError({ + statusCode: 400, + statusMessage: "threadId is required", + }); + + const result = await forumManager.getThread(threadId); + if (!result) + throw createError({ statusCode: 404, statusMessage: "Unknown thread" }); + if (result.thread.userId !== user.id && !user.admin) { + throw createError({ + statusCode: 403, + statusMessage: "Only the author or an admin may delete a thread", + }); + } + + const removed = await forumManager.deleteThread(threadId); + return { success: true, removed }; +}); diff --git a/server/server/api/v1/client/community/threads/[threadid]/posts.post.ts b/server/server/api/v1/client/community/threads/[threadid]/posts.post.ts new file mode 100644 index 000000000..374d217d1 --- /dev/null +++ b/server/server/api/v1/client/community/threads/[threadid]/posts.post.ts @@ -0,0 +1,24 @@ +import { defineClientEventHandler } from "~/server/internal/clients/event-handler"; +import { forumManager } from "~/server/internal/community"; + +interface ReplyBody { + body?: string; +} + +export default defineClientEventHandler(async (h3, { fetchUser }) => { + const user = await fetchUser(); + const threadId = getRouterParam(h3, "threadid"); + if (!threadId) + throw createError({ + statusCode: 400, + statusMessage: "threadId is required", + }); + + const payload = ((await readBody(h3)) ?? {}) as ReplyBody; + if (typeof payload.body !== "string") { + throw createError({ statusCode: 400, statusMessage: "body is required" }); + } + + const post = await forumManager.reply(user.id, threadId, payload.body); + return { success: true, post }; +}); diff --git a/server/server/api/v1/community/[gameid]/threads/index.get.ts b/server/server/api/v1/community/[gameid]/threads/index.get.ts new file mode 100644 index 000000000..63d92490f --- /dev/null +++ b/server/server/api/v1/community/[gameid]/threads/index.get.ts @@ -0,0 +1,10 @@ +import { forumManager } from "~/server/internal/community"; + +export default defineEventHandler(async (h3) => { + const gameId = getRouterParam(h3, "gameid"); + if (!gameId) + throw createError({ statusCode: 400, statusMessage: "gameId is required" }); + + const threads = await forumManager.listThreads(gameId); + return { threads, count: threads.length }; +}); diff --git a/server/server/api/v1/community/threads/[threadid].get.ts b/server/server/api/v1/community/threads/[threadid].get.ts new file mode 100644 index 000000000..611331bfe --- /dev/null +++ b/server/server/api/v1/community/threads/[threadid].get.ts @@ -0,0 +1,15 @@ +import { forumManager } from "~/server/internal/community"; + +export default defineEventHandler(async (h3) => { + const threadId = getRouterParam(h3, "threadid"); + if (!threadId) + throw createError({ + statusCode: 400, + statusMessage: "threadId is required", + }); + + const result = await forumManager.getThread(threadId); + if (!result) + throw createError({ statusCode: 404, statusMessage: "Unknown thread" }); + return result; +}); diff --git a/server/server/internal/community/__tests__/forum.test.ts b/server/server/internal/community/__tests__/forum.test.ts new file mode 100644 index 000000000..d00cdbfe4 --- /dev/null +++ b/server/server/internal/community/__tests__/forum.test.ts @@ -0,0 +1,147 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { + ForumManager, + MAX_POST_LENGTH, + MAX_THREAD_TITLE_LENGTH, + type ForumDeps, + type ForumPostRecord, + type ForumThreadRecord, +} from "../forum"; + +function createHarness() { + const threads = new Map(); + const posts: ForumPostRecord[] = []; + let next = 1; + + const deps: ForumDeps = { + prisma: { + forumThread: { + create: async ({ data }) => { + const record: ForumThreadRecord = { + id: `thread-${next++}`, + gameId: data.gameId, + userId: data.userId, + title: data.title, + body: data.body, + locked: false, + createdAt: new Date(), + updatedAt: new Date(), + }; + threads.set(record.id, record); + return record; + }, + findUnique: async ({ where }) => threads.get(where.id) ?? null, + findMany: async ({ where }) => + [...threads.values()].filter((t) => t.gameId === where.gameId), + update: async ({ where, data }) => { + const thread = threads.get(where.id); + if (!thread) throw new Error("missing thread"); + const updated: ForumThreadRecord = { ...thread, locked: data.locked }; + threads.set(updated.id, updated); + return updated; + }, + delete: async ({ where }) => { + const thread = threads.get(where.id); + if (!thread) throw new Error("missing thread"); + threads.delete(where.id); + return thread; + }, + }, + forumPost: { + create: async ({ data }) => { + const record: ForumPostRecord = { + id: `post-${next++}`, + threadId: data.threadId, + userId: data.userId, + body: data.body, + createdAt: new Date(), + updatedAt: new Date(), + }; + posts.push(record); + return record; + }, + findMany: async ({ where }) => + posts.filter((post) => post.threadId === where.threadId), + }, + }, + }; + + return { manager: new ForumManager(deps), threads, posts }; +} + +test("createThread trims input and lists newest first", async () => { + const { manager } = createHarness(); + const thread = await manager.createThread( + "user-1", + "game-1", + " Stuck on boss ", + " Any tips? ", + ); + assert.equal(thread.title, "Stuck on boss"); + assert.equal(thread.body, "Any tips?"); + assert.equal(thread.locked, false); + + const threads = await manager.listThreads("game-1"); + assert.equal(threads.length, 1); + + await assert.rejects( + () => manager.createThread("user-1", "game-1", " ", "body"), + /title is required/, + ); +}); + +test("createThread and reply enforce length caps", async () => { + const { manager } = createHarness(); + await assert.rejects( + () => + manager.createThread( + "user-1", + "game-1", + "x".repeat(MAX_THREAD_TITLE_LENGTH + 1), + "body", + ), + /at most/, + ); + + const thread = await manager.createThread("user-1", "game-1", "T", "B"); + await assert.rejects( + () => manager.reply("user-2", thread.id, "y".repeat(MAX_POST_LENGTH + 1)), + /at most/, + ); +}); + +test("reply appends to a thread and is rejected when locked", async () => { + const { manager } = createHarness(); + const thread = await manager.createThread("user-1", "game-1", "T", "B"); + + const post = await manager.reply("user-2", thread.id, "first reply"); + assert.equal(post.threadId, thread.id); + + const view = await manager.getThread(thread.id); + assert.equal(view?.thread.id, thread.id); + assert.deepEqual( + view?.posts.map((p) => p.body), + ["first reply"], + ); + + await manager.setLocked(thread.id, true); + await assert.rejects( + () => manager.reply("user-2", thread.id, "late reply"), + /locked/, + ); + + await assert.rejects( + () => manager.reply("user-2", "missing", "hi"), + /Unknown thread/, + ); +}); + +test("deleteThread reports existence and removes the thread", async () => { + const { manager } = createHarness(); + const thread = await manager.createThread("user-1", "game-1", "T", "B"); + + assert.equal(await manager.deleteThread(thread.id), true); + assert.equal(await manager.deleteThread(thread.id), false); + assert.equal(await manager.getThread(thread.id), null); +}); diff --git a/server/server/internal/community/forum.ts b/server/server/internal/community/forum.ts new file mode 100644 index 000000000..9f58df605 --- /dev/null +++ b/server/server/internal/community/forum.ts @@ -0,0 +1,181 @@ +import { createError } from "h3"; + +/** + * Per-game discussion forums (#20 community hubs). + * + * Threads and replies are plain text with hard length caps so a single post + * cannot bloat the database or the rendered page. Locked threads reject new + * replies; deletion authorization (author/admin) is enforced at the route. + */ + +export interface ForumThreadRecord { + id: string; + gameId: string; + userId: string; + title: string; + body: string; + locked: boolean; + createdAt: Date; + updatedAt: Date; +} + +export interface ForumPostRecord { + id: string; + threadId: string; + userId: string; + body: string; + createdAt: Date; + updatedAt: Date; +} + +export const MAX_THREAD_TITLE_LENGTH = 200; +export const MAX_POST_LENGTH = 20_000; + +/** Dependency surface kept structural so the manager is unit-testable. */ +export interface ForumDeps { + prisma: { + forumThread: { + create(args: { + data: { + gameId: string; + userId: string; + title: string; + body: string; + }; + }): Promise; + findUnique(args: { + where: { id: string }; + }): Promise; + findMany(args: { + where: { gameId: string }; + orderBy: { updatedAt: "asc" | "desc" }; + }): Promise; + update(args: { + where: { id: string }; + data: { locked: boolean }; + }): Promise; + delete(args: { where: { id: string } }): Promise; + }; + forumPost: { + create(args: { + data: { threadId: string; userId: string; body: string }; + }): Promise; + findMany(args: { + where: { threadId: string }; + orderBy: { createdAt: "asc" | "desc" }; + }): Promise; + }; + }; +} + +function requireText( + value: string | undefined, + field: string, + maxLength: number, +): string { + const trimmed = value?.trim() ?? ""; + if (trimmed.length === 0) { + throw createError({ + statusCode: 400, + statusMessage: `${field} is required`, + }); + } + if (trimmed.length > maxLength) { + throw createError({ + statusCode: 400, + statusMessage: `${field} must be at most ${maxLength} characters`, + }); + } + return trimmed; +} + +export class ForumManager { + constructor(private readonly deps: ForumDeps) {} + + async createThread( + userId: string, + gameId: string, + title: string, + body: string, + ): Promise { + return this.deps.prisma.forumThread.create({ + data: { + gameId, + userId, + title: requireText(title, "title", MAX_THREAD_TITLE_LENGTH), + body: requireText(body, "body", MAX_POST_LENGTH), + }, + }); + } + + /** Threads newest-updated first, matching a typical forum landing page. */ + async listThreads(gameId: string): Promise { + return this.deps.prisma.forumThread.findMany({ + where: { gameId }, + orderBy: { updatedAt: "desc" }, + }); + } + + async getThread( + threadId: string, + ): Promise<{ thread: ForumThreadRecord; posts: ForumPostRecord[] } | null> { + const thread = await this.deps.prisma.forumThread.findUnique({ + where: { id: threadId }, + }); + if (!thread) return null; + const posts = await this.deps.prisma.forumPost.findMany({ + where: { threadId }, + orderBy: { createdAt: "asc" }, + }); + return { thread, posts }; + } + + async reply( + userId: string, + threadId: string, + body: string, + ): Promise { + const thread = await this.deps.prisma.forumThread.findUnique({ + where: { id: threadId }, + }); + if (!thread) { + throw createError({ statusCode: 404, statusMessage: "Unknown thread" }); + } + if (thread.locked) { + throw createError({ statusCode: 409, statusMessage: "Thread is locked" }); + } + return this.deps.prisma.forumPost.create({ + data: { + threadId, + userId, + body: requireText(body, "body", MAX_POST_LENGTH), + }, + }); + } + + async setLocked( + threadId: string, + locked: boolean, + ): Promise { + const thread = await this.deps.prisma.forumThread.findUnique({ + where: { id: threadId }, + }); + if (!thread) { + throw createError({ statusCode: 404, statusMessage: "Unknown thread" }); + } + return this.deps.prisma.forumThread.update({ + where: { id: threadId }, + data: { locked }, + }); + } + + /** Delete a thread; returns whether it existed. Authorization is the route's. */ + async deleteThread(threadId: string): Promise { + const thread = await this.deps.prisma.forumThread.findUnique({ + where: { id: threadId }, + }); + if (!thread) return false; + await this.deps.prisma.forumThread.delete({ where: { id: threadId } }); + return true; + } +} diff --git a/server/server/internal/community/index.ts b/server/server/internal/community/index.ts index 1b7c39abc..c811b7fe3 100644 --- a/server/server/internal/community/index.ts +++ b/server/server/internal/community/index.ts @@ -1,15 +1,27 @@ import prisma from "../db/database"; import { ReviewManager, type ReviewDeps } from "./manager"; +import { ForumManager, type ForumDeps } from "./forum"; const deps: ReviewDeps = { prisma: prisma as unknown as ReviewDeps["prisma"], }; +const forumDeps: ForumDeps = { + prisma: prisma as unknown as ForumDeps["prisma"], +}; + export const reviewManager = new ReviewManager(deps); +export const forumManager = new ForumManager(forumDeps); export { ReviewManager, DEFAULT_VERIFIED_PLAYTIME_SECONDS, isVerifiedReview, } from "./manager"; export type { ReviewDeps, ReviewRecord } from "./manager"; +export { + ForumManager, + MAX_POST_LENGTH, + MAX_THREAD_TITLE_LENGTH, +} from "./forum"; +export type { ForumDeps, ForumPostRecord, ForumThreadRecord } from "./forum"; export default reviewManager; From 3db58710cba38864b2ac7b37d26db07cff149f35 Mon Sep 17 00:00:00 2001 From: John Smith Date: Mon, 14 Sep 2026 12:54:13 -0400 Subject: [PATCH 218/243] feat(desktop): show per-game forum threads on the community page Completes the forums UI for #20: - Tauri commands fetch_forum_threads / fetch_forum_thread read the public community API and deserialize into camelCase views. - community.vue loads reviews and threads together and renders a discussion section with an inline thread view. - Registered the new commands alongside fetch_game_reviews. --- desktop/main/pages/community.vue | 98 +++++++++++++++++++- desktop/src-tauri/src/community.rs | 126 ++++++++++++++++++++++++++ desktop/src-tauri/src/lib.rs | 2 + docs/implementation/roadmap-status.md | 2 +- 4 files changed, 222 insertions(+), 6 deletions(-) diff --git a/desktop/main/pages/community.vue b/desktop/main/pages/community.vue index d894ec97c..67ebb9e84 100644 --- a/desktop/main/pages/community.vue +++ b/desktop/main/pages/community.vue @@ -48,6 +48,55 @@ No reviews yet. + +
+

Discussion

+
    +
  • + +
  • +
  • + No threads yet. +
  • +
+ +
+

{{ threadDetail.thread.title }}

+

{{ threadDetail.thread.body }}

+
    +
  • + {{ post.body }} +
  • +
+
+
@@ -64,8 +113,32 @@ interface ReviewView { createdAt?: string; } +interface ForumThreadView { + id: string; + userId: string; + title: string; + body: string; + locked: boolean; + createdAt?: string; + updatedAt?: string; +} + +interface ForumPostView { + id: string; + userId: string; + body: string; + createdAt?: string; +} + +interface ForumThreadDetail { + thread: ForumThreadView; + posts: ForumPostView[]; +} + const gameId = ref(""); const reviews = ref([]); +const threads = ref([]); +const threadDetail = ref(); const loading = ref(false); const error = ref(); @@ -77,16 +150,31 @@ async function load() { } loading.value = true; error.value = undefined; + threadDetail.value = undefined; try { - reviews.value = await invoke("fetch_game_reviews", { - gameId: trimmed, - }); + const [reviewList, threadList] = await Promise.all([ + invoke("fetch_game_reviews", { gameId: trimmed }), + invoke("fetch_forum_threads", { gameId: trimmed }), + ]); + reviews.value = reviewList; + threads.value = threadList; } catch (e) { - console.warn("Failed to load reviews:", e); - error.value = "Could not load reviews."; + console.warn("Failed to load community data:", e); + error.value = "Could not load community data."; reviews.value = []; + threads.value = []; } finally { loading.value = false; } } + +async function openThread(threadId: string) { + try { + threadDetail.value = await invoke("fetch_forum_thread", { + threadId, + }); + } catch (e) { + console.warn("Failed to load thread:", e); + } +} diff --git a/desktop/src-tauri/src/community.rs b/desktop/src-tauri/src/community.rs index 256b56e8b..68840dddd 100644 --- a/desktop/src-tauri/src/community.rs +++ b/desktop/src-tauri/src/community.rs @@ -58,3 +58,129 @@ pub async fn fetch_game_reviews(game_id: String) -> Result, Stri }) .collect()) } + +#[derive(serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ForumThreadView { + pub id: String, + pub user_id: String, + pub title: String, + pub body: String, + pub locked: bool, + pub created_at: Option, + pub updated_at: Option, +} + +#[derive(serde::Deserialize)] +#[serde(rename_all = "camelCase")] +struct ForumThreadRow { + id: String, + user_id: String, + title: String, + body: String, + #[serde(default)] + locked: bool, + #[serde(default)] + created_at: Option, + #[serde(default)] + updated_at: Option, +} + +impl From for ForumThreadView { + fn from(row: ForumThreadRow) -> Self { + Self { + id: row.id, + user_id: row.user_id, + title: row.title, + body: row.body, + locked: row.locked, + created_at: row.created_at, + updated_at: row.updated_at, + } + } +} + +#[derive(serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ForumPostView { + pub id: String, + pub user_id: String, + pub body: String, + pub created_at: Option, +} + +#[derive(serde::Deserialize)] +#[serde(rename_all = "camelCase")] +struct ForumPostRow { + id: String, + user_id: String, + body: String, + #[serde(default)] + created_at: Option, +} + +impl From for ForumPostView { + fn from(row: ForumPostRow) -> Self { + Self { + id: row.id, + user_id: row.user_id, + body: row.body, + created_at: row.created_at, + } + } +} + +#[derive(serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ForumThreadDetailView { + pub thread: ForumThreadView, + pub posts: Vec, +} + +#[derive(serde::Deserialize)] +struct ForumThreadsResponse { + threads: Vec, +} + +#[derive(serde::Deserialize)] +struct ForumThreadDetailResponse { + thread: ForumThreadRow, + posts: Vec, +} + +async fn fetch_forum_json( + url: url::Url, + what: &str, +) -> Result { + let response = DROP_CLIENT_ASYNC + .get(url) + .send() + .await + .map_err(|e| e.to_string())?; + if !response.status().is_success() { + return Err(format!("failed to fetch {what}: {}", response.status())); + } + let body = response.text().await.map_err(|e| e.to_string())?; + serde_json::from_str(&body).map_err(|e| e.to_string()) +} + +/// Fetches the public forum threads for a game. +#[tauri::command] +pub async fn fetch_forum_threads(game_id: String) -> Result, String> { + let url = generate_url(&["/api/v1/community", &game_id, "threads"], &[]) + .map_err(|e| e.to_string())?; + let response: ForumThreadsResponse = fetch_forum_json(url, "forum threads").await?; + Ok(response.threads.into_iter().map(Into::into).collect()) +} + +/// Fetches a forum thread and its replies. +#[tauri::command] +pub async fn fetch_forum_thread(thread_id: String) -> Result { + let url = generate_url(&["/api/v1/community", "threads", &thread_id], &[]) + .map_err(|e| e.to_string())?; + let response: ForumThreadDetailResponse = fetch_forum_json(url, "forum thread").await?; + Ok(ForumThreadDetailView { + thread: response.thread.into(), + posts: response.posts.into_iter().map(Into::into).collect(), + }) +} diff --git a/desktop/src-tauri/src/lib.rs b/desktop/src-tauri/src/lib.rs index 280753ec0..95f089578 100644 --- a/desktop/src-tauri/src/lib.rs +++ b/desktop/src-tauri/src/lib.rs @@ -295,6 +295,8 @@ pub fn run() { provision_ludusavi, // Community fetch_game_reviews, + fetch_forum_threads, + fetch_forum_thread, // Achievements unlock_achievements, // Shader caches diff --git a/docs/implementation/roadmap-status.md b/docs/implementation/roadmap-status.md index 627d5e016..adad8c398 100644 --- a/docs/implementation/roadmap-status.md +++ b/docs/implementation/roadmap-status.md @@ -36,7 +36,7 @@ tests and a pushed commit; `🟡` usable contract/reference implementation; | #17 Content delivery | ✅ | `downpour push` CDC chunker + zstd + memory/S3 upload with binary delta reuse (`--base`) and `--branch`/`--depot` manifest metadata; torrential LAN registry, SSDP discovery, HTTP chunk fetcher with peer-chunk SHA-256 verification (`verify_chunk_sha256` / `fetch_verified_from_best_peer`); shader-cache discovery, packaging format doc (`docs/implementation/shader-cache-format.md`) and `stage_shader_caches` pre-launch staging + `drop-gamebox` index; PCGamingWiki externalized. QUIC is contract-only and deferred: `torrential/src/lan/mod.rs` defines the `ChunkFetcher` trait, HTTP is the implemented transport. | | #18 Handheld/Input/Overlay | 🟡 | `input` crate (mapping + `NullBackend` availability reporting) + Big Picture page + on-screen keyboard ✅; native uinput/ViGEm backends are **not implemented** (the `BackendKind` variants are API surface only) ⛔; Vulkan/DXGI overlay ⛔ (needs the Vulkan layer toolchain). | | #8 Achievements | ✅ | Prisma models + migration, unlock API, `drop:achievement:unlock` bus; leaderboard API (`submitScore` best-score retention, `listLeaderboard`) + `GET/POST /api/v1/client/leaderboards/:gameid/:key`; GSE `achievements.json` parser + addon bridge; RetroAchievements mapping/emission; desktop unlock toasts mounted. | -| #20/#21 Social/Federation/Commerce | ✅ | verified reviews + community page; Ed25519 receipts + webhook dispatch; federation presence + moderation (block/revoke/rate-limit) and provenance-aware ODP discovery search; Drop Workshop `mod.json` registry + subscriptions; GameBox verifiable index snapshots/mirrors; depot endpoint validation; store-scanner aggregation. Store/payment provider API keys are operator-supplied (reported). | +| #20/#21 Social/Federation/Commerce | ✅ | verified reviews + community page (now with per-game forum threads/replies); Ed25519 receipts + webhook dispatch; federation presence + moderation (block/revoke/rate-limit) and provenance-aware ODP discovery search; Drop Workshop `mod.json` registry + subscriptions; GameBox verifiable index snapshots/mirrors; depot endpoint validation; store-scanner aggregation. Store/payment provider API keys are operator-supplied (reported). | ## Hard blockers (reported, not stubbed) From 5f7ba9e42120358cefd030ba2ec823def851f645 Mon Sep 17 00:00:00 2001 From: John Smith Date: Mon, 14 Sep 2026 12:56:51 -0400 Subject: [PATCH 219/243] feat(torrential): add mDNS LAN discovery backend Phase 1 / #17 automatic peer discovery on local subnets: - New lan::mdns module: PTR query builder, a tolerant mDNS response parser (A/AAAA/PTR/SRV/TXT, compression pointers, truncation-safe) and SRV+A/AAAA peer resolution into LanPeer. - MdnsDiscovery mirrors SsdpDiscovery (bind/target/timeout, loopback testable) and exposes discover_peers(). - 5 tests (query round-trip, SRV+A resolution, compression pointers, truncated packets, loopback responder). --- docs/implementation/roadmap-status.md | 12 +- torrential/src/lan/mdns.rs | 476 ++++++++++++++++++++++++++ torrential/src/lan/mod.rs | 1 + 3 files changed, 483 insertions(+), 6 deletions(-) create mode 100644 torrential/src/lan/mdns.rs diff --git a/docs/implementation/roadmap-status.md b/docs/implementation/roadmap-status.md index adad8c398..84ab5129f 100644 --- a/docs/implementation/roadmap-status.md +++ b/docs/implementation/roadmap-status.md @@ -31,12 +31,12 @@ tests and a pushed commit; `🟡` usable contract/reference implementation; ## Tranches 4–7 -| Tranche | Status | Notes | -| :--------------------------------- | :----- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| #17 Content delivery | ✅ | `downpour push` CDC chunker + zstd + memory/S3 upload with binary delta reuse (`--base`) and `--branch`/`--depot` manifest metadata; torrential LAN registry, SSDP discovery, HTTP chunk fetcher with peer-chunk SHA-256 verification (`verify_chunk_sha256` / `fetch_verified_from_best_peer`); shader-cache discovery, packaging format doc (`docs/implementation/shader-cache-format.md`) and `stage_shader_caches` pre-launch staging + `drop-gamebox` index; PCGamingWiki externalized. QUIC is contract-only and deferred: `torrential/src/lan/mod.rs` defines the `ChunkFetcher` trait, HTTP is the implemented transport. | -| #18 Handheld/Input/Overlay | 🟡 | `input` crate (mapping + `NullBackend` availability reporting) + Big Picture page + on-screen keyboard ✅; native uinput/ViGEm backends are **not implemented** (the `BackendKind` variants are API surface only) ⛔; Vulkan/DXGI overlay ⛔ (needs the Vulkan layer toolchain). | -| #8 Achievements | ✅ | Prisma models + migration, unlock API, `drop:achievement:unlock` bus; leaderboard API (`submitScore` best-score retention, `listLeaderboard`) + `GET/POST /api/v1/client/leaderboards/:gameid/:key`; GSE `achievements.json` parser + addon bridge; RetroAchievements mapping/emission; desktop unlock toasts mounted. | -| #20/#21 Social/Federation/Commerce | ✅ | verified reviews + community page (now with per-game forum threads/replies); Ed25519 receipts + webhook dispatch; federation presence + moderation (block/revoke/rate-limit) and provenance-aware ODP discovery search; Drop Workshop `mod.json` registry + subscriptions; GameBox verifiable index snapshots/mirrors; depot endpoint validation; store-scanner aggregation. Store/payment provider API keys are operator-supplied (reported). | +| Tranche | Status | Notes | +| :--------------------------------- | :----- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| #17 Content delivery | ✅ | `downpour push` CDC chunker + zstd + memory/S3 upload with binary delta reuse (`--base`) and `--branch`/`--depot` manifest metadata; torrential LAN registry, SSDP + mDNS discovery backends, HTTP chunk fetcher with peer-chunk SHA-256 verification (`verify_chunk_sha256` / `fetch_verified_from_best_peer`); shader-cache discovery, packaging format doc (`docs/implementation/shader-cache-format.md`) and `stage_shader_caches` pre-launch staging + `drop-gamebox` index; PCGamingWiki externalized. QUIC is contract-only and deferred: `torrential/src/lan/mod.rs` defines the `ChunkFetcher` trait, HTTP is the implemented transport. | +| #18 Handheld/Input/Overlay | 🟡 | `input` crate (mapping + `NullBackend` availability reporting) + Big Picture page + on-screen keyboard ✅; native uinput/ViGEm backends are **not implemented** (the `BackendKind` variants are API surface only) ⛔; Vulkan/DXGI overlay ⛔ (needs the Vulkan layer toolchain). | +| #8 Achievements | ✅ | Prisma models + migration, unlock API, `drop:achievement:unlock` bus; leaderboard API (`submitScore` best-score retention, `listLeaderboard`) + `GET/POST /api/v1/client/leaderboards/:gameid/:key`; GSE `achievements.json` parser + addon bridge; RetroAchievements mapping/emission; desktop unlock toasts mounted. | +| #20/#21 Social/Federation/Commerce | ✅ | verified reviews + community page (now with per-game forum threads/replies); Ed25519 receipts + webhook dispatch; federation presence + moderation (block/revoke/rate-limit) and provenance-aware ODP discovery search; Drop Workshop `mod.json` registry + subscriptions; GameBox verifiable index snapshots/mirrors; depot endpoint validation; store-scanner aggregation. Store/payment provider API keys are operator-supplied (reported). | ## Hard blockers (reported, not stubbed) diff --git a/torrential/src/lan/mdns.rs b/torrential/src/lan/mdns.rs new file mode 100644 index 000000000..b3e465cc3 --- /dev/null +++ b/torrential/src/lan/mdns.rs @@ -0,0 +1,476 @@ +//! mDNS discovery backend for LAN depot peers. +//! +//! Companion to [`super::ssdp`]. Drop depot servers advertise +//! `_drop-depot._tcp.local`; a client sends a multicast DNS query and resolves +//! SRV + A/AAAA answers into [`LanPeer`]s. Message construction and parsing are +//! pure so they can be exercised with crafted packets; the socket loop is +//! tested against a loopback responder. + +use std::collections::HashMap; +use std::io; +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, UdpSocket}; +use std::time::{Duration, Instant}; + +use super::LanPeer; + +/// Standard mDNS multicast group. +pub const MDNS_MULTICAST: SocketAddr = + SocketAddr::new(IpAddr::V4(Ipv4Addr::new(224, 0, 0, 251)), 5353); + +/// Default service advertised by depot servers. +pub const DEPOT_SERVICE: &str = "_drop-depot._tcp.local"; + +const TYPE_A: u16 = 1; +const TYPE_PTR: u16 = 12; +const TYPE_TXT: u16 = 16; +const TYPE_AAAA: u16 = 28; +const TYPE_SRV: u16 = 33; +const CLASS_IN: u16 = 1; +const MAX_POINTER_HOPS: usize = 128; + +/// A parsed resource record from an mDNS response. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum MdnsRecord { + Ptr { + name: String, + target: String, + }, + Srv { + name: String, + port: u16, + target: String, + }, + A { + name: String, + address: Ipv4Addr, + }, + Aaaa { + name: String, + address: Ipv6Addr, + }, + Txt { + name: String, + values: Vec, + }, + Other { + name: String, + record_type: u16, + }, +} + +/// DNS names are case-insensitive; normalize by lowercasing and dropping the +/// trailing root dot. +#[must_use] +pub fn normalize_name(name: &str) -> String { + name.trim_end_matches('.').to_ascii_lowercase() +} + +/// Encode a dotted DNS name as length-prefixed labels terminated by a root dot. +#[must_use] +pub fn encode_name(name: &str) -> Vec { + let mut out = Vec::new(); + for label in name.split('.').filter(|label| !label.is_empty()) { + let bytes = label.as_bytes(); + let len = bytes.len().min(63); + out.push(u8::try_from(len).unwrap_or(63)); + out.extend_from_slice(&bytes[..len]); + } + out.push(0); + out +} + +/// Build a standard mDNS query for a service (PTR record). +#[must_use] +pub fn build_query(service: &str) -> Vec { + let mut out = Vec::new(); + out.extend_from_slice(&0u16.to_be_bytes()); // id 0 + out.extend_from_slice(&0u16.to_be_bytes()); // flags: standard query + out.extend_from_slice(&1u16.to_be_bytes()); // qdcount + out.extend_from_slice(&0u16.to_be_bytes()); // ancount + out.extend_from_slice(&0u16.to_be_bytes()); // nscount + out.extend_from_slice(&0u16.to_be_bytes()); // arcount + out.extend_from_slice(&encode_name(service)); + out.extend_from_slice(&TYPE_PTR.to_be_bytes()); + out.extend_from_slice(&CLASS_IN.to_be_bytes()); + out +} + +/// Decode a (possibly compressed) DNS name starting at `start`. +/// +/// Returns the decoded name and the offset to resume parsing at in the +/// original record (after the name at the original level). +fn read_name(bytes: &[u8], start: usize) -> Option<(String, usize)> { + let mut labels: Vec<&str> = Vec::new(); + let mut pos = start; + let mut resume = None; + let mut hops = 0; + + loop { + let len = *bytes.get(pos)?; + if len == 0 { + pos += 1; + if resume.is_none() { + resume = Some(pos); + } + break; + } + if len & 0xC0 == 0xC0 { + let low = *bytes.get(pos + 1)?; + let pointer = (((len as usize) & 0x3F) << 8) | low as usize; + if resume.is_none() { + resume = Some(pos + 2); + } + hops += 1; + if hops > MAX_POINTER_HOPS { + return None; + } + pos = pointer; + continue; + } + let len = len as usize; + let label = bytes.get(pos + 1..pos + 1 + len)?; + labels.push(std::str::from_utf8(label).ok()?); + pos += 1 + len; + } + + Some((labels.join("."), resume.unwrap_or(pos))) +} + +fn read_u16(bytes: &[u8], offset: usize) -> Option { + let slice = bytes.get(offset..offset + 2)?; + Some(u16::from_be_bytes([slice[0], slice[1]])) +} + +/// Parse an mDNS response packet into records. Malformed packets yield the +/// records parsed so far rather than an error, matching the tolerant SSDP path. +#[must_use] +pub fn parse_response(bytes: &[u8]) -> Vec { + if bytes.len() < 12 { + return Vec::new(); + } + let question_count = read_u16(bytes, 4).unwrap_or(0) as usize; + let answer_count = read_u16(bytes, 6).unwrap_or(0) as usize; + let authority_count = read_u16(bytes, 8).unwrap_or(0) as usize; + let additional_count = read_u16(bytes, 10).unwrap_or(0) as usize; + + let mut pos = 12; + for _ in 0..question_count { + let Some((_, next)) = read_name(bytes, pos) else { + return Vec::new(); + }; + pos = next + 4; + if pos > bytes.len() { + return Vec::new(); + } + } + + let mut records = Vec::new(); + for _ in 0..(answer_count + authority_count + additional_count) { + let Some((name, next)) = read_name(bytes, pos) else { + break; + }; + pos = next; + let (Some(record_type), Some(_class), Some(rdlength)) = ( + read_u16(bytes, pos), + read_u16(bytes, pos + 2), + read_u16(bytes, pos + 8), + ) else { + break; + }; + pos += 10; + let rdlength = rdlength as usize; + let Some(rdata) = bytes.get(pos..pos + rdlength) else { + break; + }; + let name = normalize_name(&name); + + let entry = match (record_type, rdata.len()) { + (TYPE_A, 4) => MdnsRecord::A { + name, + address: Ipv4Addr::new(rdata[0], rdata[1], rdata[2], rdata[3]), + }, + (TYPE_AAAA, 16) => { + let mut octets = [0u8; 16]; + octets.copy_from_slice(rdata); + MdnsRecord::Aaaa { + name, + address: Ipv6Addr::from(octets), + } + } + (TYPE_PTR, _) => match read_name(bytes, pos) { + Some((target, _)) => MdnsRecord::Ptr { + name, + target: normalize_name(&target), + }, + None => MdnsRecord::Other { name, record_type }, + }, + (TYPE_SRV, len) if len >= 6 => { + let port = read_u16(bytes, pos + 4).unwrap_or(0); + let target = read_name(bytes, pos + 6) + .map(|(target, _)| normalize_name(&target)) + .unwrap_or_default(); + MdnsRecord::Srv { name, port, target } + } + (TYPE_TXT, _) => { + let mut values = Vec::new(); + let mut index = 0; + while index < rdata.len() { + let len = rdata[index] as usize; + index += 1; + let Some(value) = rdata.get(index..index + len) else { + break; + }; + if let Ok(value) = std::str::from_utf8(value) { + values.push(value.to_string()); + } + index += len; + } + MdnsRecord::Txt { name, values } + } + _ => MdnsRecord::Other { name, record_type }, + }; + + records.push(entry); + pos += rdlength; + } + + records +} + +/// Resolve the first complete SRV+A/AAAA peer from a response. +#[must_use] +pub fn peer_from_records(records: &[MdnsRecord]) -> Option { + let mut addresses: HashMap<&str, IpAddr> = HashMap::new(); + for record in records { + match record { + MdnsRecord::A { name, address } => { + addresses.insert(name.as_str(), IpAddr::V4(*address)); + } + MdnsRecord::Aaaa { name, address } => { + addresses.insert(name.as_str(), IpAddr::V6(*address)); + } + _ => {} + } + } + + for record in records { + if let MdnsRecord::Srv { name, port, target } = record + && let Some(address) = addresses.get(target.as_str()) + { + return Some(LanPeer { + peer_id: name.clone(), + address: SocketAddr::new(*address, *port), + rtt_ms: 0, + throughput_kibps: 0, + }); + } + } + None +} + +/// Blocking mDNS searcher. `bind` is normally `0.0.0.0:0`; tests point `target` +/// at a loopback responder. +pub struct MdnsDiscovery { + bind: SocketAddr, + target: SocketAddr, + service: String, + timeout: Duration, +} + +impl MdnsDiscovery { + #[must_use] + pub fn new(bind: SocketAddr, target: SocketAddr, service: &str, timeout: Duration) -> Self { + Self { + bind, + target, + service: service.to_string(), + timeout, + } + } + + /// Sends an mDNS query and collects response packets until the timeout. + /// + /// # Errors + /// Returns an I/O error if the socket cannot be bound or read. + pub fn search(&self) -> io::Result>> { + let socket = UdpSocket::bind(self.bind)?; + socket.set_read_timeout(Some(self.timeout))?; + socket.send_to(&build_query(&self.service), self.target)?; + + let mut responses = Vec::new(); + let deadline = Instant::now() + self.timeout; + let mut buffer = [0u8; 4096]; + while Instant::now() < deadline { + match socket.recv_from(&mut buffer) { + Ok((size, _)) => { + let records = parse_response(&buffer[..size]); + if !records.is_empty() { + responses.push(records); + } + } + Err(error) + if error.kind() == io::ErrorKind::WouldBlock + || error.kind() == io::ErrorKind::TimedOut => + { + break; + } + Err(error) => return Err(error), + } + } + Ok(responses) + } + + /// Discovered peers, ready for the LAN registry. + /// + /// # Errors + /// Returns an I/O error if the underlying mDNS search fails. + pub fn discover_peers(&self) -> io::Result> { + Ok(self + .search()? + .iter() + .filter_map(|records| peer_from_records(records)) + .collect()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn push_u16(out: &mut Vec, value: u16) { + out.extend_from_slice(&value.to_be_bytes()); + } + + fn build_srv_a_response() -> Vec { + let mut out = Vec::new(); + push_u16(&mut out, 0); // id + push_u16(&mut out, 0x8400); // response, authoritative + push_u16(&mut out, 0); // qdcount + push_u16(&mut out, 2); // ancount + push_u16(&mut out, 0); // nscount + push_u16(&mut out, 0); // arcount + + let service_name = encode_name("drop-peer-1._drop-depot._tcp.local"); + let target_name = encode_name("peer-1.local"); + + // SRV record + out.extend_from_slice(&service_name); + push_u16(&mut out, TYPE_SRV); + push_u16(&mut out, CLASS_IN); + out.extend_from_slice(&120u32.to_be_bytes()); + let srv_len = 6 + u16::try_from(target_name.len()).unwrap_or(0); + push_u16(&mut out, srv_len); + push_u16(&mut out, 0); // priority + push_u16(&mut out, 0); // weight + push_u16(&mut out, 8080); // port + out.extend_from_slice(&target_name); + + // A record + out.extend_from_slice(&target_name); + push_u16(&mut out, TYPE_A); + push_u16(&mut out, CLASS_IN); + out.extend_from_slice(&120u32.to_be_bytes()); + push_u16(&mut out, 4); + out.extend_from_slice(&[192, 168, 1, 50]); + + out + } + + #[test] + fn builds_a_ptr_query() { + let query = build_query(DEPOT_SERVICE); + assert!(query.len() > 12); + // QDCOUNT is 1 and the question round-trips through the parser. + assert_eq!(u16::from_be_bytes([query[4], query[5]]), 1); + let Some((name, _)) = read_name(&query, 12) else { + panic!("query name should parse"); + }; + assert_eq!(name, DEPOT_SERVICE.trim_end_matches('.')); + } + + #[test] + fn resolves_a_peer_from_srv_and_a_records() { + let records = parse_response(&build_srv_a_response()); + assert_eq!(records.len(), 2); + let Some(peer) = peer_from_records(&records) else { + panic!("peer should resolve"); + }; + assert_eq!(peer.address, SocketAddr::from(([192, 168, 1, 50], 8080))); + assert_eq!(peer.peer_id, "drop-peer-1._drop-depot._tcp.local"); + } + + #[test] + fn follows_compression_pointers_in_ptr_records() { + // PTR whose rdata is a pointer back to the question-style name. + let mut out = Vec::new(); + push_u16(&mut out, 0); + push_u16(&mut out, 0x8400); + push_u16(&mut out, 0); + push_u16(&mut out, 1); // ancount + push_u16(&mut out, 0); + push_u16(&mut out, 0); + + let name_offset = out.len(); + let service = encode_name("_drop-depot._tcp.local"); + out.extend_from_slice(&service); + push_u16(&mut out, TYPE_PTR); + push_u16(&mut out, CLASS_IN); + out.extend_from_slice(&120u32.to_be_bytes()); + push_u16(&mut out, 2); // rdlength: one pointer + // Pointer to the name we just wrote. + let pointer = 0xC000u16 | u16::try_from(name_offset).unwrap_or(0); + push_u16(&mut out, pointer); + + let records = parse_response(&out); + assert_eq!( + records, + vec![MdnsRecord::Ptr { + name: "_drop-depot._tcp.local".to_string(), + target: "_drop-depot._tcp.local".to_string(), + }] + ); + } + + #[test] + fn tolerates_truncated_packets() { + let full = build_srv_a_response(); + assert_eq!(parse_response(&full[..10]).len(), 0); + assert!(parse_response(&full[..full.len() - 3]).len() <= 2); + } + + #[test] + fn discovers_a_loopback_responder() { + let Ok(responder) = UdpSocket::bind("127.0.0.1:0") else { + panic!("bind responder"); + }; + let Ok(responder_addr) = responder.local_addr() else { + panic!("responder address"); + }; + let response = build_srv_a_response(); + + let handle = std::thread::spawn(move || { + let mut buffer = [0u8; 1024]; + if let Ok((_, from)) = responder.recv_from(&mut buffer) { + let _ = responder.send_to(&response, from); + } + }); + + let discovery = MdnsDiscovery::new( + SocketAddr::from(([127, 0, 0, 1], 0)), + responder_addr, + DEPOT_SERVICE, + Duration::from_millis(500), + ); + + let Ok(peers) = discovery.discover_peers() else { + panic!("search should succeed"); + }; + handle.join().ok(); + + assert_eq!(peers.len(), 1); + assert_eq!( + peers[0].address, + SocketAddr::from(([192, 168, 1, 50], 8080)) + ); + } +} diff --git a/torrential/src/lan/mod.rs b/torrential/src/lan/mod.rs index 93f32d267..fea3e42c9 100644 --- a/torrential/src/lan/mod.rs +++ b/torrential/src/lan/mod.rs @@ -6,6 +6,7 @@ //! QUIC/HTTP transport are separate integrations; this module fixes the //! selection + expiry behaviour both must agree on. +pub mod mdns; pub mod ssdp; use std::net::SocketAddr; From b241a697ca6618c820be49090d76967352b79de2 Mon Sep 17 00:00:00 2001 From: John Smith Date: Mon, 14 Sep 2026 12:59:07 -0400 Subject: [PATCH 220/243] feat(server): purchase orders and Ed25519 ownership receipts Phase 5 / #21 indie commerce: - Prisma PurchaseOrder model + migration with User/Game back-relations. - CommerceManager creates orders through a plugin PaymentGateway's createPaymentIntent and, on a succeeded webhook, issues exactly one Ed25519 ownership receipt (idempotent re-delivery) using a fail-closed operator key (DROP_RECEIPT_SIGNING_KEY). - Client routes: POST /orders, GET /receipts, GET /receipts/:orderid. - The payment webhook now settles orders (ignoring sandbox events). - 4 manager tests (validation/intent, idempotent issuance, failure statuses, owner-scoped receipts). --- .../migration.sql | 29 +++ server/prisma/models/commerce.prisma | 30 +++ server/prisma/models/content.prisma | 1 + server/prisma/models/user.prisma | 1 + .../v1/client/commerce/orders/index.post.ts | 34 +++ .../client/commerce/receipts/[orderid].get.ts | 14 ++ .../v1/client/commerce/receipts/index.get.ts | 8 + .../api/v1/commerce/webhook/[gateway].post.ts | 13 +- .../commerce/__tests__/orders.test.ts | 193 +++++++++++++++ server/server/internal/commerce/index.ts | 42 ++++ server/server/internal/commerce/orders.ts | 219 ++++++++++++++++++ 11 files changed, 583 insertions(+), 1 deletion(-) create mode 100644 server/prisma/migrations/20260919000000_add_purchase_orders/migration.sql create mode 100644 server/prisma/models/commerce.prisma create mode 100644 server/server/api/v1/client/commerce/orders/index.post.ts create mode 100644 server/server/api/v1/client/commerce/receipts/[orderid].get.ts create mode 100644 server/server/api/v1/client/commerce/receipts/index.get.ts create mode 100644 server/server/internal/commerce/__tests__/orders.test.ts create mode 100644 server/server/internal/commerce/index.ts create mode 100644 server/server/internal/commerce/orders.ts diff --git a/server/prisma/migrations/20260919000000_add_purchase_orders/migration.sql b/server/prisma/migrations/20260919000000_add_purchase_orders/migration.sql new file mode 100644 index 000000000..1d92377ca --- /dev/null +++ b/server/prisma/migrations/20260919000000_add_purchase_orders/migration.sql @@ -0,0 +1,29 @@ +-- CreateTable +CREATE TABLE "PurchaseOrder" ( + "id" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "gameId" TEXT NOT NULL, + "amount" INTEGER NOT NULL, + "currency" TEXT NOT NULL, + "gateway" TEXT NOT NULL, + "gatewayRef" TEXT, + "status" TEXT NOT NULL DEFAULT 'pending', + "receipt" TEXT, + "signature" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "paidAt" TIMESTAMP(3), + + CONSTRAINT "PurchaseOrder_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE INDEX "PurchaseOrder_userId_idx" ON "PurchaseOrder"("userId"); + +-- CreateIndex +CREATE INDEX "PurchaseOrder_gateway_gatewayRef_idx" ON "PurchaseOrder"("gateway", "gatewayRef"); + +-- AddForeignKey +ALTER TABLE "PurchaseOrder" ADD CONSTRAINT "PurchaseOrder_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "PurchaseOrder" ADD CONSTRAINT "PurchaseOrder_gameId_fkey" FOREIGN KEY ("gameId") REFERENCES "Game"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/server/prisma/models/commerce.prisma b/server/prisma/models/commerce.prisma new file mode 100644 index 000000000..af9a76206 --- /dev/null +++ b/server/prisma/models/commerce.prisma @@ -0,0 +1,30 @@ +// Indie commerce: purchase orders and Ed25519 ownership receipts (#21). + +model PurchaseOrder { + id String @id @default(uuid()) + + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + gameId String + game Game @relation(fields: [gameId], references: [id], onDelete: Cascade) + + // Minor units (e.g. cents) to avoid floating-point money. + amount Int + currency String + + gateway String + gatewayRef String? + + // pending | succeeded | failed | refunded + status String @default("pending") + + // JSON-encoded `PurchaseReceipt` and its base64 Ed25519 signature. + receipt String? + signature String? + + createdAt DateTime @default(now()) + paidAt DateTime? + + @@index([userId]) + @@index([gateway, gatewayRef]) +} diff --git a/server/prisma/models/content.prisma b/server/prisma/models/content.prisma index dc9b35a16..efdfc0e71 100644 --- a/server/prisma/models/content.prisma +++ b/server/prisma/models/content.prisma @@ -65,6 +65,7 @@ model Game { reviews GameReview[] mods Mod[] forumThreads ForumThread[] + purchaseOrders PurchaseOrder[] developers Company[] @relation(name: "developers") publishers Company[] @relation(name: "publishers") diff --git a/server/prisma/models/user.prisma b/server/prisma/models/user.prisma index e335c81d5..287acfc2b 100644 --- a/server/prisma/models/user.prisma +++ b/server/prisma/models/user.prisma @@ -29,6 +29,7 @@ model User { modSubscriptions ModSubscription[] forumThreads ForumThread[] forumPosts ForumPost[] + purchaseOrders PurchaseOrder[] groups UserGroup[] } diff --git a/server/server/api/v1/client/commerce/orders/index.post.ts b/server/server/api/v1/client/commerce/orders/index.post.ts new file mode 100644 index 000000000..24a303477 --- /dev/null +++ b/server/server/api/v1/client/commerce/orders/index.post.ts @@ -0,0 +1,34 @@ +import { defineClientEventHandler } from "~/server/internal/clients/event-handler"; +import commerceManager from "~/server/internal/commerce"; + +interface CreateOrderBody { + gameId?: string; + amount?: number; + currency?: string; + gateway?: string; +} + +export default defineClientEventHandler(async (h3, { fetchUser }) => { + const user = await fetchUser(); + const body = ((await readBody(h3)) ?? {}) as CreateOrderBody; + if ( + typeof body.gameId !== "string" || + typeof body.amount !== "number" || + typeof body.currency !== "string" || + typeof body.gateway !== "string" + ) { + throw createError({ + statusCode: 400, + statusMessage: "gameId, amount, currency and gateway are required", + }); + } + + const result = await commerceManager.createOrder({ + userId: user.id, + gameId: body.gameId, + amount: body.amount, + currency: body.currency, + gateway: body.gateway, + }); + return { success: true, ...result }; +}); diff --git a/server/server/api/v1/client/commerce/receipts/[orderid].get.ts b/server/server/api/v1/client/commerce/receipts/[orderid].get.ts new file mode 100644 index 000000000..e008d891f --- /dev/null +++ b/server/server/api/v1/client/commerce/receipts/[orderid].get.ts @@ -0,0 +1,14 @@ +import { defineClientEventHandler } from "~/server/internal/clients/event-handler"; +import commerceManager from "~/server/internal/commerce"; + +export default defineClientEventHandler(async (h3, { fetchUser }) => { + const user = await fetchUser(); + const orderId = getRouterParam(h3, "orderid"); + if (!orderId) + throw createError({ + statusCode: 400, + statusMessage: "orderId is required", + }); + + return await commerceManager.getReceipt(user.id, orderId); +}); diff --git a/server/server/api/v1/client/commerce/receipts/index.get.ts b/server/server/api/v1/client/commerce/receipts/index.get.ts new file mode 100644 index 000000000..00a7d98bf --- /dev/null +++ b/server/server/api/v1/client/commerce/receipts/index.get.ts @@ -0,0 +1,8 @@ +import { defineClientEventHandler } from "~/server/internal/clients/event-handler"; +import commerceManager from "~/server/internal/commerce"; + +export default defineClientEventHandler(async (_h3, { fetchUser }) => { + const user = await fetchUser(); + const receipts = await commerceManager.listReceipts(user.id); + return { receipts, count: receipts.length }; +}); diff --git a/server/server/api/v1/commerce/webhook/[gateway].post.ts b/server/server/api/v1/commerce/webhook/[gateway].post.ts index b595e63de..ba13b2fbb 100644 --- a/server/server/api/v1/commerce/webhook/[gateway].post.ts +++ b/server/server/api/v1/commerce/webhook/[gateway].post.ts @@ -1,4 +1,5 @@ import pluginManager from "~/server/internal/plugins"; +import commerceManager from "~/server/internal/commerce"; import { dispatchPaymentWebhook } from "~/server/internal/commerce/webhooks"; /** @@ -31,10 +32,20 @@ export default defineEventHandler(async (h3) => { } } - return await dispatchPaymentWebhook( + const result = await dispatchPaymentWebhook( pluginManager, gatewayId, payload, headers, ); + + // Mark the order paid and issue the ownership receipt. An unknown order (for + // example a sandbox event) must not fail the provider's webhook delivery. + try { + await commerceManager.settleOrder(result); + } catch (error) { + console.warn("commerce webhook could not settle order:", error); + } + + return result; }); diff --git a/server/server/internal/commerce/__tests__/orders.test.ts b/server/server/internal/commerce/__tests__/orders.test.ts new file mode 100644 index 000000000..b91258cc1 --- /dev/null +++ b/server/server/internal/commerce/__tests__/orders.test.ts @@ -0,0 +1,193 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { + CommerceManager, + type CommerceDeps, + type OrderRecord, +} from "../orders"; +import type { PaymentGateway } from "../../plugins/types"; + +function createHarness() { + const orders = new Map(); + let next = 1; + + const gateway: PaymentGateway = { + id: "stripe", + name: "Stripe", + createPaymentIntent: async ({ orderId }) => ({ + intentId: `pi_${orderId}`, + checkoutUrl: "https://pay.example/session", + status: "pending", + }), + handleWebhook: async () => ({ + orderId: "", + status: "succeeded", + transactionId: "", + }), + }; + + const deps: CommerceDeps = { + prisma: { + purchaseOrder: { + create: async ({ data }) => { + const record: OrderRecord = { + id: `order-${next++}`, + ...data, + gatewayRef: null, + status: "pending", + receipt: null, + signature: null, + createdAt: new Date(), + paidAt: null, + }; + orders.set(record.id, record); + return record; + }, + findUnique: async ({ where }) => orders.get(where.id) ?? null, + findMany: async ({ where }) => + [...orders.values()].filter((order) => order.userId === where.userId), + update: async ({ where, data }) => { + const order = orders.get(where.id); + if (!order) throw new Error("missing order"); + const updated: OrderRecord = { ...order, ...data }; + orders.set(updated.id, updated); + return updated; + }, + }, + }, + getPaymentGateway: (id) => (id === "stripe" ? gateway : undefined), + signReceipt: (receipt) => `sig:${receipt.orderId}:${receipt.amount}`, + }; + + return { manager: new CommerceManager(deps), orders }; +} + +test("createOrder validates input and creates a payment intent", async () => { + const { manager } = createHarness(); + + await assert.rejects( + () => + manager.createOrder({ + userId: "u1", + gameId: "g1", + amount: 0, + currency: "USD", + gateway: "stripe", + }), + /positive integer/, + ); + await assert.rejects( + () => + manager.createOrder({ + userId: "u1", + gameId: "g1", + amount: 100, + currency: "us", + gateway: "stripe", + }), + /3-letter/, + ); + await assert.rejects( + () => + manager.createOrder({ + userId: "u1", + gameId: "g1", + amount: 100, + currency: "USD", + gateway: "unknown", + }), + /Unknown payment gateway/, + ); + + const result = await manager.createOrder({ + userId: "u1", + gameId: "g1", + amount: 1999, + currency: "usd", + gateway: "stripe", + }); + assert.equal(result.order.currency, "USD"); + assert.equal(result.order.gatewayRef, `pi_${result.order.id}`); + assert.equal(result.checkoutUrl, "https://pay.example/session"); +}); + +test("settleOrder issues a receipt once and is idempotent", async () => { + const { manager } = createHarness(); + const { order } = await manager.createOrder({ + userId: "u1", + gameId: "g1", + amount: 500, + currency: "USD", + gateway: "stripe", + }); + + const settled = await manager.settleOrder({ + orderId: order.id, + status: "succeeded", + transactionId: "txn-1", + }); + assert.equal(settled.status, "succeeded"); + assert.equal(settled.gatewayRef, "txn-1"); + assert.equal(settled.signature, `sig:${order.id}:500`); + assert.ok(settled.receipt); + + // Re-delivery keeps the original receipt. + const again = await manager.settleOrder({ + orderId: order.id, + status: "succeeded", + transactionId: "txn-2", + }); + assert.equal(again.signature, settled.signature); + assert.equal(again.gatewayRef, "txn-1"); +}); + +test("settleOrder records non-success statuses without a receipt", async () => { + const { manager } = createHarness(); + const { order } = await manager.createOrder({ + userId: "u1", + gameId: "g1", + amount: 100, + currency: "USD", + gateway: "stripe", + }); + + const failed = await manager.settleOrder({ + orderId: order.id, + status: "failed", + transactionId: "txn-x", + }); + assert.equal(failed.status, "failed"); + assert.equal(failed.signature, null); +}); + +test("receipts are listed and scoped to their owner", async () => { + const { manager } = createHarness(); + const { order } = await manager.createOrder({ + userId: "u1", + gameId: "g1", + amount: 100, + currency: "USD", + gateway: "stripe", + }); + await manager.settleOrder({ + orderId: order.id, + status: "succeeded", + transactionId: "txn-1", + }); + + const receipts = await manager.listReceipts("u1"); + assert.equal(receipts.length, 1); + assert.equal(receipts[0].receipt.gameId, "g1"); + + const fetched = await manager.getReceipt("u1", order.id); + assert.equal(fetched.signature, `sig:${order.id}:100`); + + await assert.rejects( + () => manager.getReceipt("someone-else", order.id), + /Unknown receipt/, + ); + await assert.rejects( + () => manager.getReceipt("u1", "missing"), + /Unknown receipt/, + ); +}); diff --git a/server/server/internal/commerce/index.ts b/server/server/internal/commerce/index.ts new file mode 100644 index 000000000..7569b7cdf --- /dev/null +++ b/server/server/internal/commerce/index.ts @@ -0,0 +1,42 @@ +import { createError } from "h3"; +import prisma from "../db/database"; +import pluginManager from "../plugins"; +import { CommerceManager, type CommerceDeps } from "./orders"; +import { signReceipt, type PurchaseReceipt } from "./receipts"; + +export const RECEIPT_SIGNING_KEY_ENV = "DROP_RECEIPT_SIGNING_KEY"; + +/** Signs a receipt with the operator-supplied Ed25519 key (fail-closed). */ +function signWithConfiguredKey(receipt: PurchaseReceipt): string { + const key = process.env[RECEIPT_SIGNING_KEY_ENV]; + if (!key || key.trim().length === 0) { + throw createError({ + statusCode: 500, + statusMessage: "Receipt signing key is not configured", + }); + } + return signReceipt(receipt, key); +} + +const deps: CommerceDeps = { + prisma: prisma as unknown as CommerceDeps["prisma"], + getPaymentGateway: (id) => pluginManager.getPaymentGateway(id), + signReceipt: signWithConfiguredKey, +}; + +export const commerceManager = new CommerceManager(deps); +export { CommerceManager }; +export type { + CommerceDeps, + CreateOrderInput, + IssuedReceipt, + OrderRecord, +} from "./orders"; +export { + canonicalizeReceipt, + signReceipt, + verifyReceipt, + type PurchaseReceipt, +} from "./receipts"; +export { dispatchPaymentWebhook } from "./webhooks"; +export default commerceManager; diff --git a/server/server/internal/commerce/orders.ts b/server/server/internal/commerce/orders.ts new file mode 100644 index 000000000..39732d24a --- /dev/null +++ b/server/server/internal/commerce/orders.ts @@ -0,0 +1,219 @@ +import { createError } from "h3"; +import type { PurchaseReceipt } from "./receipts"; +import type { PaymentGateway, PaymentWebhookResult } from "../plugins/types"; + +/** + * Indie commerce purchase orders (#21). + * + * An order is created before payment and handed to a plugin-registered + * `PaymentGateway` for a payment intent. When the provider webhook reports + * success, the server issues an Ed25519-signed ownership receipt (see + * `receipts.ts`) and marks the order paid. The receipt is portable and can be + * verified offline against the server's public key. + */ + +export interface OrderRecord { + id: string; + userId: string; + gameId: string; + amount: number; + currency: string; + gateway: string; + gatewayRef?: string | null; + status: string; + receipt?: string | null; + signature?: string | null; + createdAt: Date; + paidAt?: Date | null; +} + +export interface IssuedReceipt { + orderId: string; + receipt: PurchaseReceipt; + signature: string; +} + +export interface CreateOrderInput { + userId: string; + gameId: string; + amount: number; + currency: string; + gateway: string; +} + +/** Dependency surface kept structural so the manager is unit-testable. */ +export interface CommerceDeps { + prisma: { + purchaseOrder: { + create(args: { + data: { + userId: string; + gameId: string; + amount: number; + currency: string; + gateway: string; + }; + }): Promise; + findUnique(args: { where: { id: string } }): Promise; + findMany(args: { + where: { userId: string }; + orderBy: { createdAt: "asc" | "desc" }; + }): Promise; + update(args: { + where: { id: string }; + data: { + status?: string; + gatewayRef?: string | null; + receipt?: string | null; + signature?: string | null; + paidAt?: Date | null; + }; + }): Promise; + }; + }; + getPaymentGateway(id: string): PaymentGateway | undefined; + signReceipt(receipt: PurchaseReceipt): string; +} + +function parseReceipt(order: OrderRecord): IssuedReceipt { + if (!order.receipt || !order.signature) { + throw createError({ + statusCode: 500, + statusMessage: "Paid order is missing its receipt", + }); + } + return { + orderId: order.id, + receipt: JSON.parse(order.receipt) as PurchaseReceipt, + signature: order.signature, + }; +} + +export class CommerceManager { + constructor(private readonly deps: CommerceDeps) {} + + async createOrder(input: CreateOrderInput): Promise<{ + order: OrderRecord; + checkoutUrl?: string; + clientSecret?: string; + }> { + if (!Number.isInteger(input.amount) || input.amount <= 0) { + throw createError({ + statusCode: 400, + statusMessage: "amount must be a positive integer in minor units", + }); + } + if (!/^[A-Za-z]{3}$/.test(input.currency)) { + throw createError({ + statusCode: 400, + statusMessage: "currency must be a 3-letter ISO code", + }); + } + const gateway = this.deps.getPaymentGateway(input.gateway); + if (!gateway) { + throw createError({ + statusCode: 404, + statusMessage: `Unknown payment gateway '${input.gateway}'`, + }); + } + + const currency = input.currency.toUpperCase(); + const order = await this.deps.prisma.purchaseOrder.create({ + data: { + userId: input.userId, + gameId: input.gameId, + amount: input.amount, + currency, + gateway: input.gateway, + }, + }); + + const intent = await gateway.createPaymentIntent({ + orderId: order.id, + amount: input.amount, + currency, + metadata: { userId: input.userId, gameId: input.gameId }, + }); + + const updated = await this.deps.prisma.purchaseOrder.update({ + where: { id: order.id }, + data: { gatewayRef: intent.intentId, status: intent.status }, + }); + + return { + order: updated, + checkoutUrl: intent.checkoutUrl, + clientSecret: intent.clientSecret, + }; + } + + /** + * Apply a provider webhook result. On success the ownership receipt is issued + * exactly once; repeat deliveries are idempotent. + */ + async settleOrder(result: PaymentWebhookResult): Promise { + const order = await this.deps.prisma.purchaseOrder.findUnique({ + where: { id: result.orderId }, + }); + if (!order) { + throw createError({ + statusCode: 404, + statusMessage: `Unknown order '${result.orderId}'`, + }); + } + + if (result.status !== "succeeded") { + return this.deps.prisma.purchaseOrder.update({ + where: { id: order.id }, + data: { status: result.status }, + }); + } + + if (order.status === "succeeded" && order.signature) { + return order; + } + + const receipt: PurchaseReceipt = { + orderId: order.id, + userId: order.userId, + gameId: order.gameId, + amount: order.amount, + currency: order.currency, + issuedAt: new Date().toISOString(), + }; + const signature = this.deps.signReceipt(receipt); + + return this.deps.prisma.purchaseOrder.update({ + where: { id: order.id }, + data: { + status: "succeeded", + gatewayRef: result.transactionId || order.gatewayRef, + receipt: JSON.stringify(receipt), + signature, + paidAt: new Date(), + }, + }); + } + + /** All signed ownership receipts for a user's paid orders. */ + async listReceipts(userId: string): Promise { + const orders = await this.deps.prisma.purchaseOrder.findMany({ + where: { userId }, + orderBy: { createdAt: "desc" }, + }); + return orders + .filter((order) => order.status === "succeeded" && order.signature) + .map(parseReceipt); + } + + /** One receipt, scoped to its owner (404 for anyone else or unpaid orders). */ + async getReceipt(userId: string, orderId: string): Promise { + const order = await this.deps.prisma.purchaseOrder.findUnique({ + where: { id: orderId }, + }); + if (!order || order.userId !== userId || order.status !== "succeeded") { + throw createError({ statusCode: 404, statusMessage: "Unknown receipt" }); + } + return parseReceipt(order); + } +} From 902d6bab8ffdea3f1d632d0ee59121167ff5829a Mon Sep 17 00:00:00 2001 From: John Smith Date: Mon, 14 Sep 2026 13:00:23 -0400 Subject: [PATCH 221/243] docs(roadmap): record commerce, forums and dropworks progress --- docs/implementation/roadmap-status.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/implementation/roadmap-status.md b/docs/implementation/roadmap-status.md index 84ab5129f..ff9d29d78 100644 --- a/docs/implementation/roadmap-status.md +++ b/docs/implementation/roadmap-status.md @@ -36,7 +36,7 @@ tests and a pushed commit; `🟡` usable contract/reference implementation; | #17 Content delivery | ✅ | `downpour push` CDC chunker + zstd + memory/S3 upload with binary delta reuse (`--base`) and `--branch`/`--depot` manifest metadata; torrential LAN registry, SSDP + mDNS discovery backends, HTTP chunk fetcher with peer-chunk SHA-256 verification (`verify_chunk_sha256` / `fetch_verified_from_best_peer`); shader-cache discovery, packaging format doc (`docs/implementation/shader-cache-format.md`) and `stage_shader_caches` pre-launch staging + `drop-gamebox` index; PCGamingWiki externalized. QUIC is contract-only and deferred: `torrential/src/lan/mod.rs` defines the `ChunkFetcher` trait, HTTP is the implemented transport. | | #18 Handheld/Input/Overlay | 🟡 | `input` crate (mapping + `NullBackend` availability reporting) + Big Picture page + on-screen keyboard ✅; native uinput/ViGEm backends are **not implemented** (the `BackendKind` variants are API surface only) ⛔; Vulkan/DXGI overlay ⛔ (needs the Vulkan layer toolchain). | | #8 Achievements | ✅ | Prisma models + migration, unlock API, `drop:achievement:unlock` bus; leaderboard API (`submitScore` best-score retention, `listLeaderboard`) + `GET/POST /api/v1/client/leaderboards/:gameid/:key`; GSE `achievements.json` parser + addon bridge; RetroAchievements mapping/emission; desktop unlock toasts mounted. | -| #20/#21 Social/Federation/Commerce | ✅ | verified reviews + community page (now with per-game forum threads/replies); Ed25519 receipts + webhook dispatch; federation presence + moderation (block/revoke/rate-limit) and provenance-aware ODP discovery search; Drop Workshop `mod.json` registry + subscriptions; GameBox verifiable index snapshots/mirrors; depot endpoint validation; store-scanner aggregation. Store/payment provider API keys are operator-supplied (reported). | +| #20/#21 Social/Federation/Commerce | ✅ | verified reviews + community page (with per-game forum threads/replies); purchase orders + Ed25519 ownership receipt issuance (`DROP_RECEIPT_SIGNING_KEY`) + webhook dispatch; federation presence + moderation (block/revoke/rate-limit) and provenance-aware ODP discovery search; Drop Workshop `mod.json` registry + subscriptions; GameBox verifiable index snapshots/mirrors; depot endpoint validation; store-scanner aggregation. Store/payment provider API keys are operator-supplied (reported). | ## Hard blockers (reported, not stubbed) From 327996fc535f6786e64a9eb3a2359d00de119606 Mon Sep 17 00:00:00 2001 From: John Smith Date: Mon, 14 Sep 2026 13:04:02 -0400 Subject: [PATCH 222/243] feat(input): add a Linux uinput virtual-gamepad backend Phase 2 / #18 Drop Input: - New input::uinput module: state_to_events maps sticks/triggers/dpad to ABS events and emits every button's pressed/released state each frame. - UinputBackend opens /dev/uinput, configures EV_KEY/EV_ABS bits and axis ranges, creates the device, pushes input_event frames with SYN_REPORT, and destroys the device. create() surfaces the OS error when /dev/uinput is unavailable rather than faking a device. - supported_backends() now reports Uinput on Linux alongside the Null fallback; BackendKind docs updated. - 4 new tests (axis scaling/clamping, button+release/dpad, pre-create push error); 10 total. --- desktop/src-tauri/Cargo.lock | 3 + desktop/src-tauri/input/Cargo.toml | 3 + desktop/src-tauri/input/src/lib.rs | 35 ++- desktop/src-tauri/input/src/uinput.rs | 421 ++++++++++++++++++++++++++ docs/implementation/roadmap-status.md | 11 +- 5 files changed, 455 insertions(+), 18 deletions(-) create mode 100644 desktop/src-tauri/input/src/uinput.rs diff --git a/desktop/src-tauri/Cargo.lock b/desktop/src-tauri/Cargo.lock index 3bb76a7ff..df2c8193c 100644 --- a/desktop/src-tauri/Cargo.lock +++ b/desktop/src-tauri/Cargo.lock @@ -2646,6 +2646,9 @@ dependencies = [ [[package]] name = "input" version = "0.1.0" +dependencies = [ + "libc", +] [[package]] name = "instant" diff --git a/desktop/src-tauri/input/Cargo.toml b/desktop/src-tauri/input/Cargo.toml index 911463030..9e2adc114 100644 --- a/desktop/src-tauri/input/Cargo.toml +++ b/desktop/src-tauri/input/Cargo.toml @@ -6,4 +6,7 @@ license = "AGPL-3.0-only" [dependencies] +[target.'cfg(target_os = "linux")'.dependencies] +libc = "0.2" + [dev-dependencies] diff --git a/desktop/src-tauri/input/src/lib.rs b/desktop/src-tauri/input/src/lib.rs index 16c537738..0a3bf2c10 100644 --- a/desktop/src-tauri/input/src/lib.rs +++ b/desktop/src-tauri/input/src/lib.rs @@ -2,9 +2,11 @@ //! //! This crate owns the pure mapping logic (deadzones, gyro-to-mouse, button //! remaps) so it stays testable without touching `/dev/uinput` or a driver. -//! No native virtual-device backend exists yet: [`InputBackend`] is implemented -//! only by [`MockBackend`] (tests/dry-runs) and [`NullBackend`] (explicit -//! unavailability), and [`supported_backends`] therefore reports only `Null`. +//! On Linux a real [`uinput`] backend creates a virtual gamepad; on other +//! platforms [`NullBackend`] reports unavailability explicitly. + +#[cfg(target_os = "linux")] +pub mod uinput; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum GamepadButton { @@ -159,14 +161,11 @@ impl InputBackend for MockBackend { /// Virtual gamepad backends Drop can target. /// -/// `Uinput` and `ViGEm` are reserved API surface: no `InputBackend` -/// implementation exists for them yet, so advertising them would make callers -/// believe a device can be created. They stay in the enum so native backends -/// can be added later without breaking the public API. +/// `Uinput` is implemented on Linux ([`uinput::UinputBackend`]); `ViGEm` +/// remains reserved API surface until a Windows backend exists. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum BackendKind { /// Linux `uinput`/`evdev` virtual device (requires `/dev/uinput` access). - /// Not implemented yet. Uinput, /// Windows ViGEm bus driver. Not implemented yet. ViGEm, @@ -174,10 +173,15 @@ pub enum BackendKind { Null, } -/// Backends available on the current platform. Only [`BackendKind::Null`] is -/// reported because no native `InputBackend` exists yet; the fallback surfaces -/// a precise `BackendUnavailable` error instead of pretending a virtual device -/// is available. +/// Backends available on the current platform. +#[cfg(target_os = "linux")] +#[must_use] +pub fn supported_backends() -> &'static [BackendKind] { + &[BackendKind::Uinput, BackendKind::Null] +} + +/// Backends available on the current platform. +#[cfg(not(target_os = "linux"))] #[must_use] pub fn supported_backends() -> &'static [BackendKind] { &[BackendKind::Null] @@ -288,6 +292,11 @@ mod tests { #[test] fn supported_backends_reports_only_implemented_backends() { - assert_eq!(supported_backends(), &[BackendKind::Null]); + let backends = supported_backends(); + assert!(backends.contains(&BackendKind::Null)); + #[cfg(target_os = "linux")] + assert!(backends.contains(&BackendKind::Uinput)); + #[cfg(not(target_os = "linux"))] + assert!(!backends.contains(&BackendKind::Uinput)); } } diff --git a/desktop/src-tauri/input/src/uinput.rs b/desktop/src-tauri/input/src/uinput.rs new file mode 100644 index 000000000..89f00e864 --- /dev/null +++ b/desktop/src-tauri/input/src/uinput.rs @@ -0,0 +1,421 @@ +//! Linux `uinput` virtual gamepad backend (#18). +//! +//! Creates a virtual Xbox-style gamepad via `/dev/uinput` and pushes state as +//! `input_event` structs. Device setup and the state→event mapping are kept +//! separate: [`state_to_events`] is pure and unit-tested, while [`UinputBackend`] +//! performs the ioctls/writes. Creating a device requires write access to +//! `/dev/uinput`; when it is missing, `create()` returns the OS error rather +//! than pretending the device exists. + +use std::io; +use std::os::fd::RawFd; + +use super::{GamepadButton, GamepadState, InputBackend}; + +const UINPUT_MAX_NAME_SIZE: usize = 80; +const ABS_CNT: usize = 64; + +const EV_SYN: u16 = 0; +const EV_KEY: u16 = 1; +const EV_ABS: u16 = 3; +const SYN_REPORT: u16 = 0; + +const ABS_X: u16 = 0; +const ABS_Y: u16 = 1; +const ABS_Z: u16 = 2; +const ABS_RX: u16 = 3; +const ABS_RY: u16 = 4; +const ABS_RZ: u16 = 5; +const ABS_HAT0X: u16 = 0x10; +const ABS_HAT0Y: u16 = 0x11; + +const BTN_SOUTH: u16 = 0x130; +const BTN_EAST: u16 = 0x131; +const BTN_NORTH: u16 = 0x133; +const BTN_WEST: u16 = 0x134; +const BTN_TL: u16 = 0x136; +const BTN_TR: u16 = 0x137; +const BTN_SELECT: u16 = 0x13a; +const BTN_START: u16 = 0x13b; +const BTN_THUMBL: u16 = 0x13d; +const BTN_THUMBR: u16 = 0x13e; + +// _IOW('U', n, int) / _IO('U', n) as computed by linux/uinput.h. +const UI_SET_EVBIT: libc::c_ulong = 0x4004_5564; +const UI_SET_KEYBIT: libc::c_ulong = 0x4004_5565; +const UI_SET_ABSBIT: libc::c_ulong = 0x4004_5567; +const UI_DEV_CREATE: libc::c_ulong = 0x5501; +const UI_DEV_DESTROY: libc::c_ulong = 0x5502; + +const STICK_MAX: i32 = 32_767; +const TRIGGER_MAX: i32 = 255; + +const BUTTON_MAP: &[(GamepadButton, u16)] = &[ + (GamepadButton::South, BTN_SOUTH), + (GamepadButton::East, BTN_EAST), + (GamepadButton::North, BTN_NORTH), + (GamepadButton::West, BTN_WEST), + (GamepadButton::LeftBumper, BTN_TL), + (GamepadButton::RightBumper, BTN_TR), + (GamepadButton::LeftStick, BTN_THUMBL), + (GamepadButton::RightStick, BTN_THUMBR), + (GamepadButton::Start, BTN_START), + (GamepadButton::Select, BTN_SELECT), +]; + +/// A platform-neutral event produced by the mapping (used for tests). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct MappedEvent { + pub kind: u16, + pub code: u16, + pub value: i32, +} + +#[repr(C)] +#[derive(Clone, Copy)] +struct InputId { + bustype: u16, + vendor: u16, + product: u16, + version: u16, +} + +#[repr(C)] +#[derive(Clone, Copy)] +struct UinputUserDev { + name: [libc::c_char; UINPUT_MAX_NAME_SIZE], + id: InputId, + /// The kernel's `ff_effects_max` is 32-bit; the two padding fields after it + /// in `uinput_user_dev` are covered by the explicit arrays below. + ff_effects_max: u32, + absmax: [i32; ABS_CNT], + absmin: [i32; ABS_CNT], + absfuzz: [i32; ABS_CNT], + absflat: [i32; ABS_CNT], +} + +#[repr(C)] +#[derive(Clone, Copy)] +struct InputEvent { + time: libc::timeval, + type_: u16, + code: u16, + value: i32, +} + +/// Map a gamepad state to the absolute/key events a virtual device should emit. +/// +/// Every button is emitted on every frame (pressed = 1, released = 0) so the +/// host reliably observes releases. D-pad directions are folded onto +/// `ABS_HAT0X`/`ABS_HAT0Y`. +#[must_use] +#[allow(clippy::cast_possible_truncation)] +pub fn state_to_events(state: &GamepadState) -> Vec { + let axis = |value: f32| (value.clamp(-1.0, 1.0) * STICK_MAX as f32).round() as i32; + let trigger = |value: f32| (value.clamp(0.0, 1.0) * TRIGGER_MAX as f32).round() as i32; + + let pressed = |button: GamepadButton| state.buttons.contains(&button); + let hat = |negative: bool, positive: bool| match (negative, positive) { + (true, _) => -1, + (false, true) => 1, + _ => 0, + }; + + let mut events = vec![ + MappedEvent { + kind: EV_ABS, + code: ABS_X, + value: axis(state.left_stick.x), + }, + MappedEvent { + kind: EV_ABS, + code: ABS_Y, + value: axis(state.left_stick.y), + }, + MappedEvent { + kind: EV_ABS, + code: ABS_RX, + value: axis(state.right_stick.x), + }, + MappedEvent { + kind: EV_ABS, + code: ABS_RY, + value: axis(state.right_stick.y), + }, + MappedEvent { + kind: EV_ABS, + code: ABS_Z, + value: trigger(state.left_trigger), + }, + MappedEvent { + kind: EV_ABS, + code: ABS_RZ, + value: trigger(state.right_trigger), + }, + MappedEvent { + kind: EV_ABS, + code: ABS_HAT0X, + value: hat( + pressed(GamepadButton::DpadLeft), + pressed(GamepadButton::DpadRight), + ), + }, + MappedEvent { + kind: EV_ABS, + code: ABS_HAT0Y, + value: hat( + pressed(GamepadButton::DpadUp), + pressed(GamepadButton::DpadDown), + ), + }, + ]; + + for (button, code) in BUTTON_MAP { + events.push(MappedEvent { + kind: EV_KEY, + code: *code, + value: i32::from(pressed(*button)), + }); + } + + events +} + +/// Virtual gamepad backed by `/dev/uinput`. +#[derive(Debug, Default)] +pub struct UinputBackend { + fd: Option, +} + +impl UinputBackend { + #[must_use] + pub fn new() -> Self { + Self::default() + } + + fn ioctl(fd: RawFd, request: libc::c_ulong, value: libc::c_ulong) -> io::Result<()> { + // SAFETY: `fd` is a valid uinput file descriptor opened in `create`, and + // the request/value pair is a fixed ioctl from linux/uinput.h. + let result = unsafe { libc::ioctl(fd, request, value) }; + if result < 0 { + Err(io::Error::last_os_error()) + } else { + Ok(()) + } + } + + fn configure(fd: RawFd) -> io::Result<()> { + for event_type in [EV_KEY, EV_ABS, EV_SYN] { + Self::ioctl(fd, UI_SET_EVBIT, libc::c_ulong::from(event_type))?; + } + for (_, code) in BUTTON_MAP { + Self::ioctl(fd, UI_SET_KEYBIT, libc::c_ulong::from(*code))?; + } + for axis in [ + ABS_X, ABS_Y, ABS_Z, ABS_RX, ABS_RY, ABS_RZ, ABS_HAT0X, ABS_HAT0Y, + ] { + Self::ioctl(fd, UI_SET_ABSBIT, libc::c_ulong::from(axis))?; + } + + let mut device = UinputUserDev { + name: [0; UINPUT_MAX_NAME_SIZE], + id: InputId { + // BUS_USB, a generic Microsoft-style pad id. + bustype: 0x03, + vendor: 0x045e, + product: 0x028e, + version: 1, + }, + ff_effects_max: 0, + absmax: [0; ABS_CNT], + absmin: [0; ABS_CNT], + absfuzz: [0; ABS_CNT], + absflat: [0; ABS_CNT], + }; + for (index, byte) in b"Drop Virtual Gamepad".iter().enumerate() { + device.name[index] = *byte as libc::c_char; + } + for axis in [ABS_X, ABS_Y, ABS_RX, ABS_RY] { + let index = usize::from(axis); + device.absmin[index] = -STICK_MAX; + device.absmax[index] = STICK_MAX; + } + for axis in [ABS_Z, ABS_RZ] { + let index = usize::from(axis); + device.absmin[index] = 0; + device.absmax[index] = TRIGGER_MAX; + } + for axis in [ABS_HAT0X, ABS_HAT0Y] { + let index = usize::from(axis); + device.absmin[index] = -1; + device.absmax[index] = 1; + } + + // SAFETY: `device` is a fully initialized repr(C) uinput_user_dev. + let written = unsafe { + libc::write( + fd, + std::ptr::addr_of!(device).cast(), + std::mem::size_of::(), + ) + }; + if written < 0 { + return Err(io::Error::last_os_error()); + } + + Self::ioctl(fd, UI_DEV_CREATE, 0) + } +} + +impl InputBackend for UinputBackend { + type Error = io::Error; + + fn create(&mut self) -> Result<(), Self::Error> { + if self.fd.is_some() { + return Ok(()); + } + let path = std::ffi::CString::new("/dev/uinput") + .map_err(|_| io::Error::other("invalid uinput path"))?; + // SAFETY: opening a fixed device path with no ownership transfer concerns. + let fd = unsafe { libc::open(path.as_ptr(), libc::O_WRONLY | libc::O_NONBLOCK) }; + if fd < 0 { + return Err(io::Error::last_os_error()); + } + + match Self::configure(fd) { + Ok(()) => { + self.fd = Some(fd); + Ok(()) + } + Err(error) => { + // SAFETY: `fd` was opened above and is not stored anywhere. + unsafe { libc::close(fd) }; + Err(error) + } + } + } + + fn push_state(&mut self, state: &GamepadState) -> Result<(), Self::Error> { + let Some(fd) = self.fd else { + return Err(io::Error::other("virtual gamepad has not been created")); + }; + + let zero = libc::timeval { + tv_sec: 0, + tv_usec: 0, + }; + let mut events: Vec = state_to_events(state) + .into_iter() + .map(|event| InputEvent { + time: zero, + type_: event.kind, + code: event.code, + value: event.value, + }) + .collect(); + events.push(InputEvent { + time: zero, + type_: EV_SYN, + code: SYN_REPORT, + value: 0, + }); + + // SAFETY: `events` is a contiguous array of repr(C) input_event values, + // and `fd` is our open uinput descriptor. + let bytes = unsafe { + std::slice::from_raw_parts( + events.as_ptr().cast::(), + std::mem::size_of_val(events.as_slice()), + ) + }; + let written = unsafe { libc::write(fd, bytes.as_ptr().cast(), bytes.len()) }; + if written < 0 { + Err(io::Error::last_os_error()) + } else { + Ok(()) + } + } + + fn destroy(&mut self) -> Result<(), Self::Error> { + if let Some(fd) = self.fd.take() { + let _ = Self::ioctl(fd, UI_DEV_DESTROY, 0); + // SAFETY: `fd` is the descriptor opened in `create` and just removed. + unsafe { libc::close(fd) }; + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::Stick; + + fn value_of(events: &[MappedEvent], code: u16) -> i32 { + events + .iter() + .find(|event| event.code == code) + .map(|event| event.value) + .unwrap_or_default() + } + + #[test] + fn sticks_and_triggers_scale_to_native_ranges() { + let state = GamepadState { + left_stick: Stick::new(1.0, -1.0), + right_stick: Stick::new(0.5, 0.0), + left_trigger: 1.0, + right_trigger: 0.5, + ..Default::default() + }; + let events = state_to_events(&state); + + assert_eq!(value_of(&events, ABS_X), STICK_MAX); + assert_eq!(value_of(&events, ABS_Y), -STICK_MAX); + assert_eq!( + value_of(&events, ABS_RX), + (0.5 * STICK_MAX as f32).round() as i32 + ); + assert_eq!(value_of(&events, ABS_Z), TRIGGER_MAX); + assert_eq!(value_of(&events, ABS_RZ), 128); + } + + #[test] + fn values_are_clamped() { + let state = GamepadState { + left_stick: Stick::new(5.0, -5.0), + left_trigger: 3.0, + ..Default::default() + }; + let events = state_to_events(&state); + assert_eq!(value_of(&events, ABS_X), STICK_MAX); + assert_eq!(value_of(&events, ABS_Y), -STICK_MAX); + assert_eq!(value_of(&events, ABS_Z), TRIGGER_MAX); + } + + #[test] + fn buttons_and_dpad_emit_press_and_release() { + let mut state = GamepadState { + buttons: vec![GamepadButton::South, GamepadButton::DpadUp], + ..Default::default() + }; + let pressed = state_to_events(&state); + assert_eq!(value_of(&pressed, BTN_SOUTH), 1); + assert_eq!(value_of(&pressed, BTN_EAST), 0); + assert_eq!(value_of(&pressed, ABS_HAT0Y), -1); + assert_eq!(value_of(&pressed, ABS_HAT0X), 0); + + state.buttons.clear(); + let released = state_to_events(&state); + assert_eq!(value_of(&released, BTN_SOUTH), 0); + assert_eq!(value_of(&released, ABS_HAT0Y), 0); + } + + #[test] + fn push_state_before_create_is_an_error() { + let mut backend = UinputBackend::new(); + let result = backend.push_state(&GamepadState::default()); + assert!(result.is_err()); + assert!(backend.destroy().is_ok()); + } +} diff --git a/docs/implementation/roadmap-status.md b/docs/implementation/roadmap-status.md index ff9d29d78..2386c0ded 100644 --- a/docs/implementation/roadmap-status.md +++ b/docs/implementation/roadmap-status.md @@ -34,16 +34,17 @@ tests and a pushed commit; `🟡` usable contract/reference implementation; | Tranche | Status | Notes | | :--------------------------------- | :----- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | #17 Content delivery | ✅ | `downpour push` CDC chunker + zstd + memory/S3 upload with binary delta reuse (`--base`) and `--branch`/`--depot` manifest metadata; torrential LAN registry, SSDP + mDNS discovery backends, HTTP chunk fetcher with peer-chunk SHA-256 verification (`verify_chunk_sha256` / `fetch_verified_from_best_peer`); shader-cache discovery, packaging format doc (`docs/implementation/shader-cache-format.md`) and `stage_shader_caches` pre-launch staging + `drop-gamebox` index; PCGamingWiki externalized. QUIC is contract-only and deferred: `torrential/src/lan/mod.rs` defines the `ChunkFetcher` trait, HTTP is the implemented transport. | -| #18 Handheld/Input/Overlay | 🟡 | `input` crate (mapping + `NullBackend` availability reporting) + Big Picture page + on-screen keyboard ✅; native uinput/ViGEm backends are **not implemented** (the `BackendKind` variants are API surface only) ⛔; Vulkan/DXGI overlay ⛔ (needs the Vulkan layer toolchain). | +| #18 Handheld/Input/Overlay | 🟡 | `input` crate (mapping, deadzones, gyro) with a working Linux **uinput** virtual-gamepad backend (`state_to_events` + device create/push/destroy) and a `NullBackend` fallback ✅; Big Picture page + on-screen keyboard ✅; the Windows ViGEm backend is **not implemented** ⛔; Vulkan/DXGI overlay ⛔ (needs the Vulkan layer toolchain). | | #8 Achievements | ✅ | Prisma models + migration, unlock API, `drop:achievement:unlock` bus; leaderboard API (`submitScore` best-score retention, `listLeaderboard`) + `GET/POST /api/v1/client/leaderboards/:gameid/:key`; GSE `achievements.json` parser + addon bridge; RetroAchievements mapping/emission; desktop unlock toasts mounted. | | #20/#21 Social/Federation/Commerce | ✅ | verified reviews + community page (with per-game forum threads/replies); purchase orders + Ed25519 ownership receipt issuance (`DROP_RECEIPT_SIGNING_KEY`) + webhook dispatch; federation presence + moderation (block/revoke/rate-limit) and provenance-aware ODP discovery search; Drop Workshop `mod.json` registry + subscriptions; GameBox verifiable index snapshots/mirrors; depot endpoint validation; store-scanner aggregation. Store/payment provider API keys are operator-supplied (reported). | ## Hard blockers (reported, not stubbed) -- **Native virtual gamepad** — needs `/dev/uinput` access (Linux) or the ViGEm - bus driver (Windows); no native `InputBackend` implementation exists yet, so - `input::supported_backends()` reports only `NullBackend`, which returns a - precise `BackendUnavailable` error instead of silently no-op'ing. +- **Windows virtual gamepad** — the Linux `uinput` backend now exists + (`input::uinput::UinputBackend`, reported by `supported_backends()` on Linux); + the Windows ViGEm backend is not implemented and needs the ViGEm bus driver. + When no native backend is available, `NullBackend` returns a precise + `BackendUnavailable` error instead of silently no-op'ing. - **Vulkan/DXGI in-game overlay** — requires the Vulkan layer toolchain and a DXGI hook. Only the slot names (`overlay:panel`, `overlay:quick-access` in `desktop/main/internal/plugins/types.ts`) and the client slot registry From 633a1409448f6102fa765a3762c06adde8156987 Mon Sep 17 00:00:00 2001 From: John Smith Date: Mon, 14 Sep 2026 13:06:30 -0400 Subject: [PATCH 223/243] feat(server): add storefront price tiers Phase 5 / #21 developer publishing: - Prisma GamePrice model + migration and a Game back-relation. - PricingManager sets/lists prices and resolves only active tiers (minor units; 0 = free), with currency/amount validation. - Public GET /api/v1/store/:gameid/price?currency= and admin-only PUT /api/v1/client/commerce/prices/:gameid. - 3 manager tests (upsert, validation, active-only resolution). --- docs/implementation/roadmap-status.md | 2 +- .../migration.sql | 21 ++++ server/prisma/models/commerce.prisma | 20 ++++ server/prisma/models/content.prisma | 1 + .../v1/client/commerce/prices/[gameid].put.ts | 37 ++++++ .../server/api/v1/store/[gameid]/price.get.ts | 11 ++ .../commerce/__tests__/pricing.test.ts | 93 ++++++++++++++ server/server/internal/commerce/index.ts | 8 ++ server/server/internal/commerce/pricing.ts | 113 ++++++++++++++++++ 9 files changed, 305 insertions(+), 1 deletion(-) create mode 100644 server/prisma/migrations/20260920000000_add_game_prices/migration.sql create mode 100644 server/server/api/v1/client/commerce/prices/[gameid].put.ts create mode 100644 server/server/api/v1/store/[gameid]/price.get.ts create mode 100644 server/server/internal/commerce/__tests__/pricing.test.ts create mode 100644 server/server/internal/commerce/pricing.ts diff --git a/docs/implementation/roadmap-status.md b/docs/implementation/roadmap-status.md index 2386c0ded..ca0b582a7 100644 --- a/docs/implementation/roadmap-status.md +++ b/docs/implementation/roadmap-status.md @@ -36,7 +36,7 @@ tests and a pushed commit; `🟡` usable contract/reference implementation; | #17 Content delivery | ✅ | `downpour push` CDC chunker + zstd + memory/S3 upload with binary delta reuse (`--base`) and `--branch`/`--depot` manifest metadata; torrential LAN registry, SSDP + mDNS discovery backends, HTTP chunk fetcher with peer-chunk SHA-256 verification (`verify_chunk_sha256` / `fetch_verified_from_best_peer`); shader-cache discovery, packaging format doc (`docs/implementation/shader-cache-format.md`) and `stage_shader_caches` pre-launch staging + `drop-gamebox` index; PCGamingWiki externalized. QUIC is contract-only and deferred: `torrential/src/lan/mod.rs` defines the `ChunkFetcher` trait, HTTP is the implemented transport. | | #18 Handheld/Input/Overlay | 🟡 | `input` crate (mapping, deadzones, gyro) with a working Linux **uinput** virtual-gamepad backend (`state_to_events` + device create/push/destroy) and a `NullBackend` fallback ✅; Big Picture page + on-screen keyboard ✅; the Windows ViGEm backend is **not implemented** ⛔; Vulkan/DXGI overlay ⛔ (needs the Vulkan layer toolchain). | | #8 Achievements | ✅ | Prisma models + migration, unlock API, `drop:achievement:unlock` bus; leaderboard API (`submitScore` best-score retention, `listLeaderboard`) + `GET/POST /api/v1/client/leaderboards/:gameid/:key`; GSE `achievements.json` parser + addon bridge; RetroAchievements mapping/emission; desktop unlock toasts mounted. | -| #20/#21 Social/Federation/Commerce | ✅ | verified reviews + community page (with per-game forum threads/replies); purchase orders + Ed25519 ownership receipt issuance (`DROP_RECEIPT_SIGNING_KEY`) + webhook dispatch; federation presence + moderation (block/revoke/rate-limit) and provenance-aware ODP discovery search; Drop Workshop `mod.json` registry + subscriptions; GameBox verifiable index snapshots/mirrors; depot endpoint validation; store-scanner aggregation. Store/payment provider API keys are operator-supplied (reported). | +| #20/#21 Social/Federation/Commerce | ✅ | verified reviews + community page (with per-game forum threads/replies); storefront price tiers (`GamePrice`, admin set + public resolve); purchase orders + Ed25519 ownership receipt issuance (`DROP_RECEIPT_SIGNING_KEY`) + webhook dispatch; federation presence + moderation (block/revoke/rate-limit) and provenance-aware ODP discovery search; Drop Workshop `mod.json` registry + subscriptions; GameBox verifiable index snapshots/mirrors; depot endpoint validation; store-scanner aggregation. Store/payment provider API keys are operator-supplied (reported). | ## Hard blockers (reported, not stubbed) diff --git a/server/prisma/migrations/20260920000000_add_game_prices/migration.sql b/server/prisma/migrations/20260920000000_add_game_prices/migration.sql new file mode 100644 index 000000000..fd665b834 --- /dev/null +++ b/server/prisma/migrations/20260920000000_add_game_prices/migration.sql @@ -0,0 +1,21 @@ +-- CreateTable +CREATE TABLE "GamePrice" ( + "id" TEXT NOT NULL, + "gameId" TEXT NOT NULL, + "currency" TEXT NOT NULL, + "amount" INTEGER NOT NULL, + "active" BOOLEAN NOT NULL DEFAULT true, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "GamePrice_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "GamePrice_gameId_currency_key" ON "GamePrice"("gameId", "currency"); + +-- CreateIndex +CREATE INDEX "GamePrice_gameId_idx" ON "GamePrice"("gameId"); + +-- AddForeignKey +ALTER TABLE "GamePrice" ADD CONSTRAINT "GamePrice_gameId_fkey" FOREIGN KEY ("gameId") REFERENCES "Game"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/server/prisma/models/commerce.prisma b/server/prisma/models/commerce.prisma index af9a76206..75ee1fcde 100644 --- a/server/prisma/models/commerce.prisma +++ b/server/prisma/models/commerce.prisma @@ -28,3 +28,23 @@ model PurchaseOrder { @@index([userId]) @@index([gateway, gatewayRef]) } + +// Per-game price tiers shown in the storefront (#21 developer publishing). + +model GamePrice { + id String @id @default(uuid()) + + gameId String + game Game @relation(fields: [gameId], references: [id], onDelete: Cascade) + + currency String + // Minor units (e.g. cents). 0 marks a free title. + amount Int + active Boolean @default(true) + + createdAt DateTime @default(now()) + updatedAt DateTime @default(now()) @updatedAt + + @@unique([gameId, currency]) + @@index([gameId]) +} diff --git a/server/prisma/models/content.prisma b/server/prisma/models/content.prisma index efdfc0e71..ea5e73fa5 100644 --- a/server/prisma/models/content.prisma +++ b/server/prisma/models/content.prisma @@ -66,6 +66,7 @@ model Game { mods Mod[] forumThreads ForumThread[] purchaseOrders PurchaseOrder[] + prices GamePrice[] developers Company[] @relation(name: "developers") publishers Company[] @relation(name: "publishers") diff --git a/server/server/api/v1/client/commerce/prices/[gameid].put.ts b/server/server/api/v1/client/commerce/prices/[gameid].put.ts new file mode 100644 index 000000000..d7fa619d6 --- /dev/null +++ b/server/server/api/v1/client/commerce/prices/[gameid].put.ts @@ -0,0 +1,37 @@ +import { defineClientEventHandler } from "~/server/internal/clients/event-handler"; +import { pricingManager } from "~/server/internal/commerce"; + +interface SetPriceBody { + currency?: string; + amount?: number; + active?: boolean; +} + +export default defineClientEventHandler(async (h3, { fetchUser }) => { + const user = await fetchUser(); + if (!user.admin) { + throw createError({ + statusCode: 403, + statusMessage: "Admin access required to set prices", + }); + } + const gameId = getRouterParam(h3, "gameid"); + if (!gameId) + throw createError({ statusCode: 400, statusMessage: "gameId is required" }); + + const body = ((await readBody(h3)) ?? {}) as SetPriceBody; + if (typeof body.currency !== "string" || typeof body.amount !== "number") { + throw createError({ + statusCode: 400, + statusMessage: "currency and amount are required", + }); + } + + const price = await pricingManager.setPrice( + gameId, + body.currency, + body.amount, + body.active ?? true, + ); + return { success: true, price }; +}); diff --git a/server/server/api/v1/store/[gameid]/price.get.ts b/server/server/api/v1/store/[gameid]/price.get.ts new file mode 100644 index 000000000..17d2bab74 --- /dev/null +++ b/server/server/api/v1/store/[gameid]/price.get.ts @@ -0,0 +1,11 @@ +import { pricingManager } from "~/server/internal/commerce"; + +export default defineEventHandler(async (h3) => { + const gameId = getRouterParam(h3, "gameid"); + if (!gameId) + throw createError({ statusCode: 400, statusMessage: "gameId is required" }); + + const currency = (getQuery(h3).currency as string | undefined) ?? "USD"; + const price = await pricingManager.resolvePrice(gameId, currency); + return { gameId, currency: currency.toUpperCase(), price }; +}); diff --git a/server/server/internal/commerce/__tests__/pricing.test.ts b/server/server/internal/commerce/__tests__/pricing.test.ts new file mode 100644 index 000000000..57530cc04 --- /dev/null +++ b/server/server/internal/commerce/__tests__/pricing.test.ts @@ -0,0 +1,93 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { PricingManager, type PriceRecord, type PricingDeps } from "../pricing"; + +function createHarness() { + const prices = new Map(); + let next = 1; + const key = (gameId: string, currency: string) => `${gameId}:${currency}`; + + const deps: PricingDeps = { + prisma: { + gamePrice: { + findUnique: async ({ where }) => + prices.get( + key(where.gameId_currency.gameId, where.gameId_currency.currency), + ) ?? null, + create: async ({ data }) => { + const record: PriceRecord = { + id: `price-${next++}`, + ...data, + createdAt: new Date(), + updatedAt: new Date(), + }; + prices.set(key(data.gameId, data.currency), record); + return record; + }, + update: async ({ where, data }) => { + const entry = [...prices.values()].find((p) => p.id === where.id); + if (!entry) throw new Error("missing price"); + const updated: PriceRecord = { + ...entry, + ...data, + updatedAt: new Date(), + }; + prices.set(key(updated.gameId, updated.currency), updated); + return updated; + }, + findMany: async ({ where }) => + [...prices.values()] + .filter((p) => p.gameId === where.gameId) + .sort((a, b) => a.currency.localeCompare(b.currency)), + }, + }, + }; + + return { manager: new PricingManager(deps), prices }; +} + +test("setPrice creates then updates the same (game, currency) price", async () => { + const { manager } = createHarness(); + + const created = await manager.setPrice("game-1", "usd", 1999); + assert.equal(created.currency, "USD"); + assert.equal(created.amount, 1999); + assert.equal(created.active, true); + + const updated = await manager.setPrice("game-1", "USD", 1499, false); + assert.equal(updated.id, created.id); + assert.equal(updated.amount, 1499); + assert.equal(updated.active, false); +}); + +test("setPrice validates currency and amount", async () => { + const { manager } = createHarness(); + await assert.rejects(() => manager.setPrice("game-1", "us", 100), /3-letter/); + await assert.rejects( + () => manager.setPrice("game-1", "USD", -1), + /non-negative integer/, + ); + await assert.rejects( + () => manager.setPrice("game-1", "USD", 1.5), + /non-negative integer/, + ); +}); + +test("resolvePrice only returns active prices and lists all tiers", async () => { + const { manager } = createHarness(); + await manager.setPrice("game-1", "USD", 0); + await manager.setPrice("game-1", "EUR", 999); + + const free = await manager.resolvePrice("game-1", "usd"); + assert.equal(free?.amount, 0); + + await manager.setPrice("game-1", "EUR", 999, false); + assert.equal(await manager.resolvePrice("game-1", "eur"), null); + assert.equal(await manager.resolvePrice("game-2", "usd"), null); + + const tiers = await manager.listPrices("game-1"); + assert.deepEqual( + tiers.map((tier) => tier.currency), + ["EUR", "USD"], + ); +}); diff --git a/server/server/internal/commerce/index.ts b/server/server/internal/commerce/index.ts index 7569b7cdf..df0d39faa 100644 --- a/server/server/internal/commerce/index.ts +++ b/server/server/internal/commerce/index.ts @@ -2,6 +2,7 @@ import { createError } from "h3"; import prisma from "../db/database"; import pluginManager from "../plugins"; import { CommerceManager, type CommerceDeps } from "./orders"; +import { PricingManager, type PricingDeps } from "./pricing"; import { signReceipt, type PurchaseReceipt } from "./receipts"; export const RECEIPT_SIGNING_KEY_ENV = "DROP_RECEIPT_SIGNING_KEY"; @@ -32,6 +33,13 @@ export type { IssuedReceipt, OrderRecord, } from "./orders"; + +const pricingDeps: PricingDeps = { + prisma: prisma as unknown as PricingDeps["prisma"], +}; +export const pricingManager = new PricingManager(pricingDeps); +export { PricingManager }; +export type { PriceRecord, PricingDeps } from "./pricing"; export { canonicalizeReceipt, signReceipt, diff --git a/server/server/internal/commerce/pricing.ts b/server/server/internal/commerce/pricing.ts new file mode 100644 index 000000000..fbbd28687 --- /dev/null +++ b/server/server/internal/commerce/pricing.ts @@ -0,0 +1,113 @@ +import { createError } from "h3"; + +/** + * Storefront price tiers (#21 developer publishing). + * + * Prices are stored per game and currency in minor units (e.g. cents); an + * amount of `0` marks a free title. Only active prices are resolvable, so a + * price can be withdrawn without deleting its history. + */ + +export interface PriceRecord { + id: string; + gameId: string; + currency: string; + amount: number; + active: boolean; + createdAt: Date; + updatedAt: Date; +} + +/** Dependency surface kept structural so the manager is unit-testable. */ +export interface PricingDeps { + prisma: { + gamePrice: { + findUnique(args: { + where: { gameId_currency: { gameId: string; currency: string } }; + }): Promise; + create(args: { + data: { + gameId: string; + currency: string; + amount: number; + active: boolean; + }; + }): Promise; + update(args: { + where: { id: string }; + data: { amount?: number; active?: boolean }; + }): Promise; + findMany(args: { + where: { gameId: string }; + orderBy?: { currency: "asc" | "desc" }; + }): Promise; + }; + }; +} + +function normalizeCurrency(currency: string): string { + if (!/^[A-Za-z]{3}$/.test(currency)) { + throw createError({ + statusCode: 400, + statusMessage: "currency must be a 3-letter ISO code", + }); + } + return currency.toUpperCase(); +} + +function validateAmount(amount: number): number { + if (!Number.isInteger(amount) || amount < 0) { + throw createError({ + statusCode: 400, + statusMessage: "amount must be a non-negative integer in minor units", + }); + } + return amount; +} + +export class PricingManager { + constructor(private readonly deps: PricingDeps) {} + + /** Create or update a game's price for a currency. */ + async setPrice( + gameId: string, + currency: string, + amount: number, + active = true, + ): Promise { + const code = normalizeCurrency(currency); + const value = validateAmount(amount); + const existing = await this.deps.prisma.gamePrice.findUnique({ + where: { gameId_currency: { gameId, currency: code } }, + }); + if (existing) { + return this.deps.prisma.gamePrice.update({ + where: { id: existing.id }, + data: { amount: value, active }, + }); + } + return this.deps.prisma.gamePrice.create({ + data: { gameId, currency: code, amount: value, active }, + }); + } + + async listPrices(gameId: string): Promise { + return this.deps.prisma.gamePrice.findMany({ + where: { gameId }, + orderBy: { currency: "asc" }, + }); + } + + /** The active price for a currency, or `null` when none is set. */ + async resolvePrice( + gameId: string, + currency: string, + ): Promise { + const record = await this.deps.prisma.gamePrice.findUnique({ + where: { + gameId_currency: { gameId, currency: normalizeCurrency(currency) }, + }, + }); + return record?.active ? record : null; + } +} From de1b8080141dd49f43f7a98d30a554b27093b7c3 Mon Sep 17 00:00:00 2001 From: John Smith Date: Mon, 14 Sep 2026 13:10:52 -0400 Subject: [PATCH 224/243] feat(community): add a public screenshot gallery Phase 4 / #20 community hubs (screenshot galleries): - ObjectHandler.setPermissions replaces an object's permission list. - ScreenshotManager gains getPublicAllByGame and setVisibility, which synchronize object permissions (owner read; anonymous:read when public) with the screenshot's visibility. - Public GET /api/v1/community/:gameid/screenshots and owner-only POST /api/v1/client/screenshots/:id/visibility. - Desktop community page renders the gallery via the existing object asset protocol; new fetch_game_screenshots Tauri command. - 2 tests for the permission mapping. --- desktop/main/pages/community.vue | 33 ++++++++++++++- desktop/src-tauri/src/community.rs | 42 +++++++++++++++++++ desktop/src-tauri/src/lib.rs | 1 + docs/implementation/roadmap-status.md | 2 +- .../screenshots/[id]/visibility.post.ts | 32 ++++++++++++++ .../v1/community/[gameid]/screenshots.get.ts | 11 +++++ .../server/internal/objects/objectHandler.ts | 13 ++++++ .../screenshots/__tests__/permissions.test.ts | 16 +++++++ server/server/internal/screenshots/index.ts | 38 +++++++++++++++++ .../internal/screenshots/permissions.ts | 12 ++++++ 10 files changed, 198 insertions(+), 2 deletions(-) create mode 100644 server/server/api/v1/client/screenshots/[id]/visibility.post.ts create mode 100644 server/server/api/v1/community/[gameid]/screenshots.get.ts create mode 100644 server/server/internal/screenshots/__tests__/permissions.test.ts create mode 100644 server/server/internal/screenshots/permissions.ts diff --git a/desktop/main/pages/community.vue b/desktop/main/pages/community.vue index 67ebb9e84..7ab1ba154 100644 --- a/desktop/main/pages/community.vue +++ b/desktop/main/pages/community.vue @@ -49,6 +49,25 @@ +
+

Screenshots

+
+ Game screenshot +
+

+ No screenshots yet. +

+
+

Discussion

    @@ -103,6 +122,7 @@ diff --git a/desktop/src-tauri/src/lib.rs b/desktop/src-tauri/src/lib.rs index 8ff06d899..ffe9e93df 100644 --- a/desktop/src-tauri/src/lib.rs +++ b/desktop/src-tauri/src/lib.rs @@ -74,6 +74,7 @@ mod settings; mod presence; mod shaders; mod webrtc; +mod workshop; mod updates; use achievements::*; @@ -90,6 +91,7 @@ use settings::*; use presence::*; use shaders::*; use webrtc::*; +use workshop::*; use crate::scheduler::scheduler_task; @@ -307,6 +309,9 @@ pub fn run() { clear_presence, // WebRTC fetch_ice_config, + // Workshop + fetch_workshop_subscriptions, + fetch_workshop_mod, // Achievements unlock_achievements, // Shader caches diff --git a/desktop/src-tauri/src/workshop.rs b/desktop/src-tauri/src/workshop.rs new file mode 100644 index 000000000..fb11c4258 --- /dev/null +++ b/desktop/src-tauri/src/workshop.rs @@ -0,0 +1,42 @@ +use database::borrow_db_checked; +use remote::auth::generate_authorization_header; +use remote::requests::generate_url; +use remote::utils::DROP_CLIENT_ASYNC; + +async fn fetch_authenticated_json(path: &[&str]) -> Result { + if borrow_db_checked().auth.is_none() { + return Err("not authenticated".to_string()); + } + + let url = generate_url(path, &[]).map_err(|e| e.to_string())?; + let response = DROP_CLIENT_ASYNC + .get(url) + .header("Authorization", generate_authorization_header()) + .send() + .await + .map_err(|e| e.to_string())?; + + if !response.status().is_success() { + return Err(format!("request failed: {}", response.status())); + } + + response + .json::() + .await + .map_err(|e| e.to_string()) +} + +/// The authenticated user's Workshop mod subscriptions (with mod metadata). +#[tauri::command] +pub async fn fetch_workshop_subscriptions() -> Result { + fetch_authenticated_json(&["/api/v1/client/workshop/subscriptions"]).await +} + +/// A Workshop mod's releases (used to read the `mod.json` manifest). +#[tauri::command] +pub async fn fetch_workshop_mod( + game_id: String, + key: String, +) -> Result { + fetch_authenticated_json(&["/api/v1/workshop", &game_id, "mods", &key]).await +} diff --git a/docs/implementation/roadmap-status.md b/docs/implementation/roadmap-status.md index 0cdb8b67d..a5a87f691 100644 --- a/docs/implementation/roadmap-status.md +++ b/docs/implementation/roadmap-status.md @@ -31,13 +31,13 @@ tests and a pushed commit; `🟡` usable contract/reference implementation; ## Tranches 4–7 -| Tranche | Status | Notes | -| :--------------------------------- | :----- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| #17 Content delivery | ✅ | `downpour push` CDC chunker + zstd + memory/S3 upload with binary delta reuse (`--base`) and `--branch`/`--depot` manifest metadata; torrential LAN registry, SSDP + mDNS discovery backends, HTTP chunk fetcher with peer-chunk SHA-256 verification (`verify_chunk_sha256` / `fetch_verified_from_best_peer`); shader-cache discovery, packaging format doc (`docs/implementation/shader-cache-format.md`) and `stage_shader_caches` pre-launch staging + `drop-gamebox` index; PCGamingWiki externalized. QUIC is contract-only and deferred: `torrential/src/lan/mod.rs` defines the `ChunkFetcher` trait, HTTP is the implemented transport. | -| #18 Handheld/Input/Overlay | 🟡 | `input` crate (mapping, deadzones, gyro) with a working Linux **uinput** virtual-gamepad backend (`state_to_events` + device create/push/destroy) and a `NullBackend` fallback ✅; Big Picture page + on-screen keyboard ✅; the Windows ViGEm backend is **not implemented** ⛔; Vulkan/DXGI overlay ⛔ (needs the Vulkan layer toolchain). | -| #19 Dropworks SDK & Multiplayer | 🟡 | Server REST contract (`/api/v1/dropworks/session`, `/achievement`, `/leaderboard`, `/presence`) with token-derived users, leaderboard best-score retention and presence wired to the local presence service ✅; bindings: TypeScript reference client, Rust crate, tested C ABI (`libdropworks`, C smoke test), C# P/Invoke wrapper (buildable smoke test) and Godot 4 GDScript (`HTTPClient`, headless test) covering session/achievement/leaderboard/presence ✅; The server-side ICE config provider (STUN + ephemeral coturn TURN credentials at GET /api/v1/client/ice) and the federation signaling relay (POST/GET/DELETE /signaling/:instanceId) are in place ✅; a client peer module (webview `RTCPeerConnection` + `fetch_ice_config` Tauri command) consumes them ✅; automated NAT hole-punching validation remains ⛔. | -| #8 Achievements | ✅ | Prisma models + migration, unlock API, `drop:achievement:unlock` bus; leaderboard API (`submitScore` best-score retention, `listLeaderboard`) + `GET/POST /api/v1/client/leaderboards/:gameid/:key`; GSE `achievements.json` parser + addon bridge; RetroAchievements mapping/emission; desktop unlock toasts mounted. | -| #20/#21 Social/Federation/Commerce | ✅ | local rich presence (desktop reports on launch/exit + online list) + verified reviews + community page (with per-game forum threads/replies and a public screenshot gallery); storefront price tiers (`GamePrice`, admin set + public resolve); purchase orders + Ed25519 ownership receipt issuance (`DROP_RECEIPT_SIGNING_KEY`) + webhook dispatch; developer crash telemetry (bounded report ingestion + admin view); federation presence + moderation (block/revoke/rate-limit), key rotation, opt-in library sharing scopes, and provenance-aware ODP discovery search; Drop Workshop `mod.json` registry + subscriptions + desktop load-order/conflict planner; GameBox verifiable index snapshots/mirrors + contribution moderation; depot endpoint validation; store-scanner aggregation. Store/payment provider API keys are operator-supplied (reported). | +| Tranche | Status | Notes | +| :--------------------------------- | :----- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| #17 Content delivery | ✅ | `downpour push` CDC chunker + zstd + memory/S3 upload with binary delta reuse (`--base`) and `--branch`/`--depot` manifest metadata; torrential LAN registry, SSDP + mDNS discovery backends, HTTP chunk fetcher with peer-chunk SHA-256 verification (`verify_chunk_sha256` / `fetch_verified_from_best_peer`); shader-cache discovery, packaging format doc (`docs/implementation/shader-cache-format.md`) and `stage_shader_caches` pre-launch staging + `drop-gamebox` index; PCGamingWiki externalized. QUIC is contract-only and deferred: `torrential/src/lan/mod.rs` defines the `ChunkFetcher` trait, HTTP is the implemented transport. | +| #18 Handheld/Input/Overlay | 🟡 | `input` crate (mapping, deadzones, gyro) with a working Linux **uinput** virtual-gamepad backend (`state_to_events` + device create/push/destroy) and a `NullBackend` fallback ✅; Big Picture page + on-screen keyboard ✅; the Windows ViGEm backend is **not implemented** ⛔; Vulkan/DXGI overlay ⛔ (needs the Vulkan layer toolchain). | +| #19 Dropworks SDK & Multiplayer | 🟡 | Server REST contract (`/api/v1/dropworks/session`, `/achievement`, `/leaderboard`, `/presence`) with token-derived users, leaderboard best-score retention and presence wired to the local presence service ✅; bindings: TypeScript reference client, Rust crate, tested C ABI (`libdropworks`, C smoke test), C# P/Invoke wrapper (buildable smoke test) and Godot 4 GDScript (`HTTPClient`, headless test) covering session/achievement/leaderboard/presence ✅; The server-side ICE config provider (STUN + ephemeral coturn TURN credentials at GET /api/v1/client/ice) and the federation signaling relay (POST/GET/DELETE /signaling/:instanceId) are in place ✅; a client peer module (webview `RTCPeerConnection` + `fetch_ice_config` Tauri command) consumes them ✅; automated NAT hole-punching validation remains ⛔. | +| #8 Achievements | ✅ | Prisma models + migration, unlock API, `drop:achievement:unlock` bus; leaderboard API (`submitScore` best-score retention, `listLeaderboard`) + `GET/POST /api/v1/client/leaderboards/:gameid/:key`; GSE `achievements.json` parser + addon bridge; RetroAchievements mapping/emission; desktop unlock toasts mounted. | +| #20/#21 Social/Federation/Commerce | ✅ | local rich presence (desktop reports on launch/exit + online list) + verified reviews + community page (with per-game forum threads/replies and a public screenshot gallery); storefront price tiers (`GamePrice`, admin set + public resolve); purchase orders + Ed25519 ownership receipt issuance (`DROP_RECEIPT_SIGNING_KEY`) + webhook dispatch; developer crash telemetry (bounded report ingestion + admin view); federation presence + moderation (block/revoke/rate-limit), key rotation, opt-in library sharing scopes, and provenance-aware ODP discovery search; Drop Workshop `mod.json` registry + subscriptions + desktop mod manager (subscription planning page with load order/dependency/conflict analysis); GameBox verifiable index snapshots/mirrors + contribution moderation; depot endpoint validation; store-scanner aggregation. Store/payment provider API keys are operator-supplied (reported). | ## Hard blockers (reported, not stubbed) From 501b040ca79dfc0c21b50adf3d19aaeea7c31dff Mon Sep 17 00:00:00 2001 From: John Smith Date: Mon, 14 Sep 2026 13:56:20 -0400 Subject: [PATCH 240/243] feat(input): add shareable controller profiles Phase 2 / #18 per-game input configurations: - input::profile: ControllerProfile (name, deadzone, gyro sensitivity, button remaps) with serde JSON round-trip, validate_profile, remap and apply_profile (deadzone + gyro scale + remaps). - 4 tests (JSON round-trip, validation errors, apply, remap dedupe); clippy clean and cargo check -p drop-app passes. --- desktop/src-tauri/Cargo.lock | 2 + desktop/src-tauri/input/Cargo.toml | 2 + desktop/src-tauri/input/src/lib.rs | 8 +- desktop/src-tauri/input/src/profile.rs | 209 +++++++++++++++++++++++++ docs/implementation/roadmap-status.md | 2 +- 5 files changed, 221 insertions(+), 2 deletions(-) create mode 100644 desktop/src-tauri/input/src/profile.rs diff --git a/desktop/src-tauri/Cargo.lock b/desktop/src-tauri/Cargo.lock index df2c8193c..ac76c3e02 100644 --- a/desktop/src-tauri/Cargo.lock +++ b/desktop/src-tauri/Cargo.lock @@ -2648,6 +2648,8 @@ name = "input" version = "0.1.0" dependencies = [ "libc", + "serde", + "serde_json", ] [[package]] diff --git a/desktop/src-tauri/input/Cargo.toml b/desktop/src-tauri/input/Cargo.toml index 9e2adc114..e856f3535 100644 --- a/desktop/src-tauri/input/Cargo.toml +++ b/desktop/src-tauri/input/Cargo.toml @@ -5,6 +5,8 @@ edition = "2024" license = "AGPL-3.0-only" [dependencies] +serde = { version = "1", features = ["derive"] } +serde_json = "1" [target.'cfg(target_os = "linux")'.dependencies] libc = "0.2" diff --git a/desktop/src-tauri/input/src/lib.rs b/desktop/src-tauri/input/src/lib.rs index 0a3bf2c10..271626e6e 100644 --- a/desktop/src-tauri/input/src/lib.rs +++ b/desktop/src-tauri/input/src/lib.rs @@ -8,7 +8,13 @@ #[cfg(target_os = "linux")] pub mod uinput; -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub mod profile; + +pub use profile::{ + ButtonRemap, ControllerProfile, ProfileError, apply_profile, remap_buttons, validate_profile, +}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)] pub enum GamepadButton { South, East, diff --git a/desktop/src-tauri/input/src/profile.rs b/desktop/src-tauri/input/src/profile.rs new file mode 100644 index 000000000..9ffc0d2e4 --- /dev/null +++ b/desktop/src-tauri/input/src/profile.rs @@ -0,0 +1,209 @@ +//! Per-game controller layout profiles (#18 shareable input configs). +//! +//! A profile is a serializable, shareable description of a controller layout: +//! stick deadzone, gyro tuning, and button remaps. Profiles are validated before +//! use and applied to a [`GamepadState`] by [`apply_profile`]. + +use serde::{Deserialize, Serialize}; +use std::fmt; + +use crate::{GamepadButton, GamepadState, Gyro, normalize_sticks}; + +/// One button remap (press `from`, report `to`). +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub struct ButtonRemap { + pub from: GamepadButton, + pub to: GamepadButton, +} + +/// A shareable controller layout profile. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct ControllerProfile { + pub name: String, + /// Stick deadzone in `0.0..=1.0`. + pub deadzone: f32, + /// Gyro multiplier (1.0 = unchanged). + pub gyro_sensitivity: f32, + #[serde(default)] + pub remaps: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ProfileError { + EmptyName, + InvalidDeadzone, + InvalidGyroSensitivity, + DuplicateRemapFrom(GamepadButton), +} + +impl fmt::Display for ProfileError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ProfileError::EmptyName => write!(formatter, "profile name is required"), + ProfileError::InvalidDeadzone => { + write!(formatter, "deadzone must be within 0.0..=1.0") + } + ProfileError::InvalidGyroSensitivity => { + write!( + formatter, + "gyro sensitivity must be a finite, non-negative number" + ) + } + ProfileError::DuplicateRemapFrom(button) => { + write!(formatter, "duplicate remap source: {button:?}") + } + } + } +} + +impl std::error::Error for ProfileError {} + +/// Validate a profile before applying or sharing it. +/// +/// # Errors +/// Returns a [`ProfileError`] for an empty name, out-of-range deadzone, +/// non-finite/negative gyro sensitivity, or two remaps sharing a source button. +pub fn validate_profile(profile: &ControllerProfile) -> Result<(), ProfileError> { + if profile.name.trim().is_empty() { + return Err(ProfileError::EmptyName); + } + if !(0.0..=1.0).contains(&profile.deadzone) { + return Err(ProfileError::InvalidDeadzone); + } + if !profile.gyro_sensitivity.is_finite() || profile.gyro_sensitivity < 0.0 { + return Err(ProfileError::InvalidGyroSensitivity); + } + let mut seen = Vec::new(); + for remap in &profile.remaps { + if seen.contains(&remap.from) { + return Err(ProfileError::DuplicateRemapFrom(remap.from)); + } + seen.push(remap.from); + } + Ok(()) +} + +/// Rewrite button presses through a profile's remaps, preserving order and +/// de-duplicating results. +#[must_use] +pub fn remap_buttons(buttons: &[GamepadButton], remaps: &[ButtonRemap]) -> Vec { + let mut mapped = Vec::new(); + for button in buttons { + let target = remaps + .iter() + .find(|remap| remap.from == *button) + .map_or(*button, |remap| remap.to); + if !mapped.contains(&target) { + mapped.push(target); + } + } + mapped +} + +/// Apply a profile to a gamepad state: deadzone the sticks, scale gyro, and +/// remap buttons. +#[must_use] +pub fn apply_profile(state: &GamepadState, profile: &ControllerProfile) -> GamepadState { + let mut next = normalize_sticks(state, profile.deadzone); + next.gyro = state.gyro.map(|gyro| Gyro { + pitch: gyro.pitch * profile.gyro_sensitivity, + yaw: gyro.yaw * profile.gyro_sensitivity, + roll: gyro.roll * profile.gyro_sensitivity, + }); + next.buttons = remap_buttons(&state.buttons, &profile.remaps); + next +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::Stick; + + fn profile() -> ControllerProfile { + ControllerProfile { + name: "Deck".to_string(), + deadzone: 0.2, + gyro_sensitivity: 2.0, + remaps: vec![ButtonRemap { + from: GamepadButton::South, + to: GamepadButton::East, + }], + } + } + + #[test] + fn round_trips_through_json() { + let serialized = serde_json::to_string(&profile()).expect("serialize"); + let restored: ControllerProfile = serde_json::from_str(&serialized).expect("deserialize"); + assert_eq!(restored, profile()); + } + + #[test] + fn validation_rejects_bad_values() { + let mut bad_name = profile(); + bad_name.name = " ".to_string(); + assert_eq!(validate_profile(&bad_name), Err(ProfileError::EmptyName)); + + let mut bad_deadzone = profile(); + bad_deadzone.deadzone = 1.5; + assert_eq!( + validate_profile(&bad_deadzone), + Err(ProfileError::InvalidDeadzone) + ); + + let mut bad_gyro = profile(); + bad_gyro.gyro_sensitivity = -1.0; + assert_eq!( + validate_profile(&bad_gyro), + Err(ProfileError::InvalidGyroSensitivity) + ); + + let mut duplicate = profile(); + duplicate.remaps.push(ButtonRemap { + from: GamepadButton::South, + to: GamepadButton::North, + }); + assert_eq!( + validate_profile(&duplicate), + Err(ProfileError::DuplicateRemapFrom(GamepadButton::South)) + ); + } + + #[test] + fn apply_profile_deadzones_scales_and_remaps() { + let state = GamepadState { + left_stick: Stick::new(0.1, 0.9), + buttons: vec![GamepadButton::South], + gyro: Some(Gyro { + pitch: 1.0, + yaw: 2.0, + roll: 0.0, + }), + ..Default::default() + }; + let applied = apply_profile(&state, &profile()); + + assert_eq!(applied.left_stick.x, 0.0); + assert!(applied.left_stick.y > 0.8); + assert_eq!(applied.buttons, vec![GamepadButton::East]); + + let applied_gyro = applied.gyro.expect("gyro preserved"); + assert_eq!(applied_gyro.yaw, 4.0); + } + + #[test] + fn remap_dedupes_repeated_targets() { + let buttons = [GamepadButton::South, GamepadButton::North]; + let remaps = [ + ButtonRemap { + from: GamepadButton::South, + to: GamepadButton::East, + }, + ButtonRemap { + from: GamepadButton::North, + to: GamepadButton::East, + }, + ]; + assert_eq!(remap_buttons(&buttons, &remaps), vec![GamepadButton::East]); + } +} diff --git a/docs/implementation/roadmap-status.md b/docs/implementation/roadmap-status.md index a5a87f691..ead91bc44 100644 --- a/docs/implementation/roadmap-status.md +++ b/docs/implementation/roadmap-status.md @@ -34,7 +34,7 @@ tests and a pushed commit; `🟡` usable contract/reference implementation; | Tranche | Status | Notes | | :--------------------------------- | :----- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | #17 Content delivery | ✅ | `downpour push` CDC chunker + zstd + memory/S3 upload with binary delta reuse (`--base`) and `--branch`/`--depot` manifest metadata; torrential LAN registry, SSDP + mDNS discovery backends, HTTP chunk fetcher with peer-chunk SHA-256 verification (`verify_chunk_sha256` / `fetch_verified_from_best_peer`); shader-cache discovery, packaging format doc (`docs/implementation/shader-cache-format.md`) and `stage_shader_caches` pre-launch staging + `drop-gamebox` index; PCGamingWiki externalized. QUIC is contract-only and deferred: `torrential/src/lan/mod.rs` defines the `ChunkFetcher` trait, HTTP is the implemented transport. | -| #18 Handheld/Input/Overlay | 🟡 | `input` crate (mapping, deadzones, gyro) with a working Linux **uinput** virtual-gamepad backend (`state_to_events` + device create/push/destroy) and a `NullBackend` fallback ✅; Big Picture page + on-screen keyboard ✅; the Windows ViGEm backend is **not implemented** ⛔; Vulkan/DXGI overlay ⛔ (needs the Vulkan layer toolchain). | +| #18 Handheld/Input/Overlay | 🟡 | `input` crate (mapping, deadzones, gyro, shareable `ControllerProfile` JSON with validation/remap/apply) with a working Linux **uinput** virtual-gamepad backend (`state_to_events` + device create/push/destroy) and a `NullBackend` fallback ✅; Big Picture page + on-screen keyboard ✅; the Windows ViGEm backend is **not implemented** ⛔; Vulkan/DXGI overlay ⛔ (needs the Vulkan layer toolchain). | | #19 Dropworks SDK & Multiplayer | 🟡 | Server REST contract (`/api/v1/dropworks/session`, `/achievement`, `/leaderboard`, `/presence`) with token-derived users, leaderboard best-score retention and presence wired to the local presence service ✅; bindings: TypeScript reference client, Rust crate, tested C ABI (`libdropworks`, C smoke test), C# P/Invoke wrapper (buildable smoke test) and Godot 4 GDScript (`HTTPClient`, headless test) covering session/achievement/leaderboard/presence ✅; The server-side ICE config provider (STUN + ephemeral coturn TURN credentials at GET /api/v1/client/ice) and the federation signaling relay (POST/GET/DELETE /signaling/:instanceId) are in place ✅; a client peer module (webview `RTCPeerConnection` + `fetch_ice_config` Tauri command) consumes them ✅; automated NAT hole-punching validation remains ⛔. | | #8 Achievements | ✅ | Prisma models + migration, unlock API, `drop:achievement:unlock` bus; leaderboard API (`submitScore` best-score retention, `listLeaderboard`) + `GET/POST /api/v1/client/leaderboards/:gameid/:key`; GSE `achievements.json` parser + addon bridge; RetroAchievements mapping/emission; desktop unlock toasts mounted. | | #20/#21 Social/Federation/Commerce | ✅ | local rich presence (desktop reports on launch/exit + online list) + verified reviews + community page (with per-game forum threads/replies and a public screenshot gallery); storefront price tiers (`GamePrice`, admin set + public resolve); purchase orders + Ed25519 ownership receipt issuance (`DROP_RECEIPT_SIGNING_KEY`) + webhook dispatch; developer crash telemetry (bounded report ingestion + admin view); federation presence + moderation (block/revoke/rate-limit), key rotation, opt-in library sharing scopes, and provenance-aware ODP discovery search; Drop Workshop `mod.json` registry + subscriptions + desktop mod manager (subscription planning page with load order/dependency/conflict analysis); GameBox verifiable index snapshots/mirrors + contribution moderation; depot endpoint validation; store-scanner aggregation. Store/payment provider API keys are operator-supplied (reported). | From 65313e61c4420d4d114c4013e2b3e8a538bcf6ed Mon Sep 17 00:00:00 2001 From: John Smith Date: Mon, 14 Sep 2026 13:58:34 -0400 Subject: [PATCH 241/243] feat(power): add handheld battery policy and command Phase 2 / #18 handheld power management: - New power crate: PowerPolicy/PowerState/PowerDecision with a pure decide() and a Linux sysfs battery reader (parse_linux_power_state + read_linux_power_state). - download_power_decision Tauri command maps the current battery state to allow/throttle/pause for the download manager (unrestricted when unknown). - 5 tests (mains/charging, thresholds, clamping, validation, sysfs parsing); clippy clean and cargo check -p drop-app passes. --- desktop/src-tauri/Cargo.lock | 5 + desktop/src-tauri/Cargo.toml | 2 + desktop/src-tauri/power/Cargo.toml | 10 ++ desktop/src-tauri/power/src/lib.rs | 230 ++++++++++++++++++++++++++ desktop/src-tauri/src/handheld.rs | 46 ++++++ desktop/src-tauri/src/lib.rs | 4 + docs/implementation/roadmap-status.md | 2 +- 7 files changed, 298 insertions(+), 1 deletion(-) create mode 100644 desktop/src-tauri/power/Cargo.toml create mode 100644 desktop/src-tauri/power/src/lib.rs create mode 100644 desktop/src-tauri/src/handheld.rs diff --git a/desktop/src-tauri/Cargo.lock b/desktop/src-tauri/Cargo.lock index ac76c3e02..488630d0f 100644 --- a/desktop/src-tauri/Cargo.lock +++ b/desktop/src-tauri/Cargo.lock @@ -1387,6 +1387,7 @@ dependencies = [ "native_model", "page_size", "parking_lot 0.12.5", + "power", "process", "rand 0.10.2", "rayon", @@ -4104,6 +4105,10 @@ dependencies = [ "zerovec", ] +[[package]] +name = "power" +version = "0.1.0" + [[package]] name = "powerfmt" version = "0.2.0" diff --git a/desktop/src-tauri/Cargo.toml b/desktop/src-tauri/Cargo.toml index fbf7be491..215f0ec0a 100644 --- a/desktop/src-tauri/Cargo.toml +++ b/desktop/src-tauri/Cargo.toml @@ -15,6 +15,7 @@ members = [ "download_manager", "games", "input", + "power", "process", "remote", "shader_cache", @@ -68,6 +69,7 @@ rand = "0.10.2" rayon = "1.10.0" regex = "1.11.1" remote = { path = "./remote", version = "0.1.0" } # remote +power = { path = "./power", version = "0.1.0" } # handheld power policy shader_cache = { path = "./shader_cache", version = "0.1.0" } # shader cache hooks reqwest = { version = "0.12.28", default-features = false, features = [ "blocking", diff --git a/desktop/src-tauri/power/Cargo.toml b/desktop/src-tauri/power/Cargo.toml new file mode 100644 index 000000000..62867dc1f --- /dev/null +++ b/desktop/src-tauri/power/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "power" +version = "0.1.0" +edition = "2024" +license = "AGPL-3.0-only" + +[dependencies] + +[lib] +path = "src/lib.rs" diff --git a/desktop/src-tauri/power/src/lib.rs b/desktop/src-tauri/power/src/lib.rs new file mode 100644 index 000000000..51bbff22e --- /dev/null +++ b/desktop/src-tauri/power/src/lib.rs @@ -0,0 +1,230 @@ +//! Handheld power management policy (#18). +//! +//! Pure decision logic that maps battery state to download behaviour. The +//! desktop feeds it a [`PowerState`] (from the OS battery API) and applies the +//! resulting [`PowerDecision`] to the download manager: pause on critically low +//! battery, throttle on low battery, and run unthrottled when charging or on +//! mains power. + +/// Current power source / battery state. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct PowerState { + /// True when running on battery rather than mains power. + pub on_battery: bool, + /// Battery charge percentage in `0.0..=100.0`. + pub battery_percent: f32, + /// True while the battery is actively charging. + pub charging: bool, +} + +/// Thresholds controlling the decision. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct PowerPolicy { + /// At or below this percentage, pause downloads. + pub pause_below_percent: f32, + /// At or below this percentage, throttle downloads. + pub throttle_below_percent: f32, + /// Multiplier applied when throttling (`0.0..=1.0`). + pub throttle_factor: f32, +} + +impl Default for PowerPolicy { + fn default() -> Self { + Self { + pause_below_percent: 5.0, + throttle_below_percent: 25.0, + throttle_factor: 0.5, + } + } +} + +impl PowerPolicy { + /// Validate the thresholds. + /// + /// # Errors + /// Returns a message when the factor is outside `0.0..=1.0` or the pause + /// threshold is not below the throttle threshold. + pub fn validate(&self) -> Result<(), String> { + if !self.throttle_factor.is_finite() || !(0.0..=1.0).contains(&self.throttle_factor) { + return Err("throttle_factor must be within 0.0..=1.0".to_string()); + } + if self.pause_below_percent >= self.throttle_below_percent { + return Err("pause_below_percent must be below throttle_below_percent".to_string()); + } + Ok(()) + } +} + +/// What the download manager should do. +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum PowerDecision { + Allow, + Throttle(f32), + Pause, +} + +/// Decide download behaviour for a power state under a policy. +#[must_use] +pub fn decide(state: PowerState, policy: PowerPolicy) -> PowerDecision { + if state.charging || !state.on_battery { + return PowerDecision::Allow; + } + + let percent = state.battery_percent.clamp(0.0, 100.0); + if percent <= policy.pause_below_percent { + PowerDecision::Pause + } else if percent <= policy.throttle_below_percent { + PowerDecision::Throttle(policy.throttle_factor.clamp(0.0, 1.0)) + } else { + PowerDecision::Allow + } +} + +/// Parse the sysfs `capacity`/`status` files of a battery into a [`PowerState`]. +#[must_use] +pub fn parse_linux_power_state(capacity: &str, status: &str) -> PowerState { + let battery_percent = capacity + .trim() + .parse::() + .unwrap_or(0.0) + .clamp(0.0, 100.0); + let status = status.trim().to_ascii_lowercase(); + let charging = status == "charging" || status == "full"; + let on_battery = status == "discharging"; + PowerState { + on_battery, + battery_percent, + charging, + } +} + +/// Read the first battery under `/sys/class/power_supply`. +/// +/// Returns `None` on non-Linux platforms or when no battery is present, so the +/// caller can fall back to an unthrottled policy. +#[cfg(target_os = "linux")] +#[must_use] +pub fn read_linux_power_state() -> Option { + let entries = std::fs::read_dir("/sys/class/power_supply").ok()?; + for entry in entries.flatten() { + let name = entry.file_name().to_string_lossy().to_ascii_uppercase(); + if !name.starts_with("BAT") { + continue; + } + let path = entry.path(); + let capacity = std::fs::read_to_string(path.join("capacity")).ok()?; + let status = std::fs::read_to_string(path.join("status")).ok()?; + return Some(parse_linux_power_state(&capacity, &status)); + } + None +} + +#[cfg(test)] +mod tests { + use super::*; + + fn battery(percent: f32) -> PowerState { + PowerState { + on_battery: true, + battery_percent: percent, + charging: false, + } + } + + #[test] + fn mains_and_charging_are_unrestricted() { + let policy = PowerPolicy::default(); + assert_eq!( + decide( + PowerState { + on_battery: false, + battery_percent: 10.0, + charging: false, + }, + policy + ), + PowerDecision::Allow + ); + assert_eq!( + decide( + PowerState { + on_battery: true, + battery_percent: 3.0, + charging: true, + }, + policy + ), + PowerDecision::Allow + ); + } + + #[test] + fn thresholds_pause_and_throttle_on_battery() { + let policy = PowerPolicy::default(); + assert_eq!(decide(battery(3.0), policy), PowerDecision::Pause); + assert_eq!(decide(battery(5.0), policy), PowerDecision::Pause); + assert_eq!(decide(battery(20.0), policy), PowerDecision::Throttle(0.5)); + assert_eq!(decide(battery(80.0), policy), PowerDecision::Allow); + } + + #[test] + fn clamps_out_of_range_inputs() { + let policy = PowerPolicy { + throttle_factor: 5.0, + ..Default::default() + }; + // Factor is clamped on use (validation still reports it). + assert_eq!(decide(battery(20.0), policy), PowerDecision::Throttle(1.0)); + assert_eq!(decide(battery(150.0), policy), PowerDecision::Allow); + } + + #[test] + fn policy_validation_rejects_bad_thresholds() { + assert!(PowerPolicy::default().validate().is_ok()); + assert!( + PowerPolicy { + throttle_factor: 1.5, + ..Default::default() + } + .validate() + .is_err() + ); + assert!( + PowerPolicy { + pause_below_percent: 30.0, + throttle_below_percent: 20.0, + ..Default::default() + } + .validate() + .is_err() + ); + } + + #[test] + fn parses_linux_power_state() { + assert_eq!( + parse_linux_power_state("42\n", "Discharging\n"), + PowerState { + on_battery: true, + battery_percent: 42.0, + charging: false, + } + ); + assert_eq!( + parse_linux_power_state("100", "Full"), + PowerState { + on_battery: false, + battery_percent: 100.0, + charging: true, + } + ); + assert_eq!( + parse_linux_power_state("garbage", "Unknown"), + PowerState { + on_battery: false, + battery_percent: 0.0, + charging: false, + } + ); + } +} diff --git a/desktop/src-tauri/src/handheld.rs b/desktop/src-tauri/src/handheld.rs new file mode 100644 index 000000000..28c667cec --- /dev/null +++ b/desktop/src-tauri/src/handheld.rs @@ -0,0 +1,46 @@ +use power::{decide, PowerDecision, PowerPolicy, PowerState}; + +#[derive(serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct DownloadPowerDecision { + /// One of `"allow"`, `"throttle"`, or `"pause"`. + pub decision: String, + /// Throttle multiplier when `decision == "throttle"`. + pub factor: Option, +} + +#[cfg(target_os = "linux")] +fn current_power_state() -> Option { + power::read_linux_power_state() +} + +#[cfg(not(target_os = "linux"))] +fn current_power_state() -> Option { + None +} + +/// Handheld power policy decision for the download manager. When the battery +/// state is unknown (desktop, or no battery) downloads are unrestricted. +#[tauri::command] +pub fn download_power_decision() -> DownloadPowerDecision { + let state = current_power_state().unwrap_or(PowerState { + on_battery: false, + battery_percent: 100.0, + charging: false, + }); + + match decide(state, PowerPolicy::default()) { + PowerDecision::Allow => DownloadPowerDecision { + decision: "allow".to_string(), + factor: None, + }, + PowerDecision::Throttle(factor) => DownloadPowerDecision { + decision: "throttle".to_string(), + factor: Some(factor), + }, + PowerDecision::Pause => DownloadPowerDecision { + decision: "pause".to_string(), + factor: None, + }, + } +} diff --git a/desktop/src-tauri/src/lib.rs b/desktop/src-tauri/src/lib.rs index ffe9e93df..01715ca5e 100644 --- a/desktop/src-tauri/src/lib.rs +++ b/desktop/src-tauri/src/lib.rs @@ -63,6 +63,7 @@ mod client; mod cloud_saves; mod collections; mod community; +mod handheld; mod download_manager; mod downloads; mod games; @@ -81,6 +82,7 @@ use achievements::*; use client::*; use cloud_saves::*; use community::*; +use handheld::*; use download_manager::*; use downloads::*; use games::*; @@ -309,6 +311,8 @@ pub fn run() { clear_presence, // WebRTC fetch_ice_config, + // Handheld power + download_power_decision, // Workshop fetch_workshop_subscriptions, fetch_workshop_mod, diff --git a/docs/implementation/roadmap-status.md b/docs/implementation/roadmap-status.md index ead91bc44..ef5018e66 100644 --- a/docs/implementation/roadmap-status.md +++ b/docs/implementation/roadmap-status.md @@ -34,7 +34,7 @@ tests and a pushed commit; `🟡` usable contract/reference implementation; | Tranche | Status | Notes | | :--------------------------------- | :----- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | #17 Content delivery | ✅ | `downpour push` CDC chunker + zstd + memory/S3 upload with binary delta reuse (`--base`) and `--branch`/`--depot` manifest metadata; torrential LAN registry, SSDP + mDNS discovery backends, HTTP chunk fetcher with peer-chunk SHA-256 verification (`verify_chunk_sha256` / `fetch_verified_from_best_peer`); shader-cache discovery, packaging format doc (`docs/implementation/shader-cache-format.md`) and `stage_shader_caches` pre-launch staging + `drop-gamebox` index; PCGamingWiki externalized. QUIC is contract-only and deferred: `torrential/src/lan/mod.rs` defines the `ChunkFetcher` trait, HTTP is the implemented transport. | -| #18 Handheld/Input/Overlay | 🟡 | `input` crate (mapping, deadzones, gyro, shareable `ControllerProfile` JSON with validation/remap/apply) with a working Linux **uinput** virtual-gamepad backend (`state_to_events` + device create/push/destroy) and a `NullBackend` fallback ✅; Big Picture page + on-screen keyboard ✅; the Windows ViGEm backend is **not implemented** ⛔; Vulkan/DXGI overlay ⛔ (needs the Vulkan layer toolchain). | +| #18 Handheld/Input/Overlay | 🟡 | `input` crate (mapping, deadzones, gyro, shareable `ControllerProfile` JSON with validation/remap/apply) with a working Linux **uinput** virtual-gamepad backend (`state_to_events` + device create/push/destroy) and a `NullBackend` fallback ✅; Big Picture page + on-screen keyboard ✅; handheld power policy (battery-aware allow/throttle/pause + `download_power_decision` command) ✅; the Windows ViGEm backend is **not implemented** ⛔; Vulkan/DXGI overlay ⛔ (needs the Vulkan layer toolchain). | | #19 Dropworks SDK & Multiplayer | 🟡 | Server REST contract (`/api/v1/dropworks/session`, `/achievement`, `/leaderboard`, `/presence`) with token-derived users, leaderboard best-score retention and presence wired to the local presence service ✅; bindings: TypeScript reference client, Rust crate, tested C ABI (`libdropworks`, C smoke test), C# P/Invoke wrapper (buildable smoke test) and Godot 4 GDScript (`HTTPClient`, headless test) covering session/achievement/leaderboard/presence ✅; The server-side ICE config provider (STUN + ephemeral coturn TURN credentials at GET /api/v1/client/ice) and the federation signaling relay (POST/GET/DELETE /signaling/:instanceId) are in place ✅; a client peer module (webview `RTCPeerConnection` + `fetch_ice_config` Tauri command) consumes them ✅; automated NAT hole-punching validation remains ⛔. | | #8 Achievements | ✅ | Prisma models + migration, unlock API, `drop:achievement:unlock` bus; leaderboard API (`submitScore` best-score retention, `listLeaderboard`) + `GET/POST /api/v1/client/leaderboards/:gameid/:key`; GSE `achievements.json` parser + addon bridge; RetroAchievements mapping/emission; desktop unlock toasts mounted. | | #20/#21 Social/Federation/Commerce | ✅ | local rich presence (desktop reports on launch/exit + online list) + verified reviews + community page (with per-game forum threads/replies and a public screenshot gallery); storefront price tiers (`GamePrice`, admin set + public resolve); purchase orders + Ed25519 ownership receipt issuance (`DROP_RECEIPT_SIGNING_KEY`) + webhook dispatch; developer crash telemetry (bounded report ingestion + admin view); federation presence + moderation (block/revoke/rate-limit), key rotation, opt-in library sharing scopes, and provenance-aware ODP discovery search; Drop Workshop `mod.json` registry + subscriptions + desktop mod manager (subscription planning page with load order/dependency/conflict analysis); GameBox verifiable index snapshots/mirrors + contribution moderation; depot endpoint validation; store-scanner aggregation. Store/payment provider API keys are operator-supplied (reported). | From 02296fd9e1ce563aa32c72b34524128a857c1567 Mon Sep 17 00:00:00 2001 From: John Smith Date: Mon, 14 Sep 2026 14:01:03 -0400 Subject: [PATCH 242/243] docs(roadmap): add remaining-work map and refresh verification --- docs/implementation/roadmap-status.md | 32 +++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/docs/implementation/roadmap-status.md b/docs/implementation/roadmap-status.md index ef5018e66..4c7d25204 100644 --- a/docs/implementation/roadmap-status.md +++ b/docs/implementation/roadmap-status.md @@ -63,17 +63,41 @@ tests and a pushed commit; `🟡` usable contract/reference implementation; fallback). `@droposs/plugin-sdk` documents `ScopedGameScanner.findFiles` and is bumped to `0.5.0` (npm publish pending). +## Remaining work (tracked) + +Concrete, still-open items mapped to their tracking issues: + +- **Client WebRTC hole-punching validation** — the server ICE config, the + federation signaling relay, and the webview peer module exist; a live + two-peer NAT-traversal run is not automated (`drop-federation` #4, `drop` #19). +- **Seedbox torrent-backed depot + streaming** — depot registration, health, and + the registry exist; piece-level range reads from torrent data and + play-while-download are not implemented (`drop-seedbox` #3/#4/#5). +- **GameBox import-pipeline integration** — fingerprinting, `/identify`, save + paths, mirrors and moderation exist; wiring identification into the core + import pipeline is not done (`drop-gamebox` #5). +- **Multi-store unified bridge** — store scanners + desktop aggregation exist; + launching/tracking games imported from other stores is not wired + (`drop` #21). +- **GSE sidecar staging** — `gse-engine` builds a CLI binary; the client addon + does not invoke it yet (`drop-gse` M4). +- **Windows ViGEm backend** and **Vulkan/DXGI overlay** — blocked on external + toolchains (see Hard blockers). + ## Verification (all green) ```sh -pnpm --filter drop run test # 21/21 files +pnpm --filter drop run test # 22/22 files pnpm --filter drop run typecheck # 0 pnpm -C desktop/main run typecheck # 0 pnpm -C desktop/main run lint # 0 errors -cargo +nightly test -p cloud_saves -p database -p input -p shader_cache +pnpm -C desktop/main run test # 16 tests (planner, toasts, plugins) +cargo +nightly test -p cloud_saves -p database -p input -p power -p shader_cache + # input 14, power 5 cd torrential && cargo +nightly test # 43 cd cli && cargo +nightly test # 9 -pnpm -r run test # drop-plugin-sdk -# drop-gamebox (29), drop-federation (47), drop-seedbox (31), drop-gse +cd drop-gse/packages/gse-engine && cargo test # 32 lib + 3 CLI +# drop-gamebox 29, drop-federation 52, drop-seedbox 33, drop-gse # (client 19 + server 15), and all 17 plugin repos: build + tests green +# dropworks-sdk: TypeScript 4, Rust 9, C smoke, C# smoke, GDScript headless ``` From 37bb20e2c910ff62bf182d2456c9651a46d74638 Mon Sep 17 00:00:00 2001 From: John Smith Date: Mon, 14 Sep 2026 18:11:12 -0400 Subject: [PATCH 243/243] fix(server): harden plugin route patterns, i18n keys, and container user - Escape regex metacharacters when compiling plugin route patterns and cap the pattern length, so a plugin-supplied pattern cannot trigger ReDoS (semgrep detect-non-literal-regexp). - Guard localisation key traversal against __proto__/constructor/prototype and use own-property checks (semgrep prototype-pollution-loop). - Run the runtime image as an unprivileged user (uid 10001) instead of root (semgrep missing-user). Refs #68 --- Dockerfile | 12 ++++ server/i18n/scripts/utils.ts | 28 +++++++- .../plugins/__tests__/plugins.test.ts | 41 ++++++++++++ server/server/internal/plugins/manager.ts | 66 ++++++++++++++++--- 4 files changed, 136 insertions(+), 11 deletions(-) diff --git a/Dockerfile b/Dockerfile index e2977840e..6fe9ea57a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -99,6 +99,18 @@ COPY --from=build-system /app/server/build ./startup COPY --from=build-system /app/server/build/nginx.conf /nginx.conf COPY --from=torrential-build /build/torrential/target/release/torrential /usr/bin/ +# Run as an unprivileged user. Drop (port 4000) and nginx (port 3000) both bind +# unprivileged ports. Operators using bind-mounted /data or /library must make +# the mount writable by uid 10001, or run the container with a matching --user. +RUN groupadd --system --gid 10001 drop \ + && useradd --system --uid 10001 --gid drop --home-dir /app --shell /usr/sbin/nologin drop \ + && mkdir -p /pnpm /data /library \ + && chown -R drop:drop /app /pnpm /data /library + +ENV HOME="/app" +# Numeric id so hadolint can verify it; matches the `drop` user created above. +USER 10001:10001 + ENV LIBRARY="/library" ENV DATA="/data" ENV NGINX_CONFIG="/nginx.conf" diff --git a/server/i18n/scripts/utils.ts b/server/i18n/scripts/utils.ts index 949a59eae..f20a0536b 100644 --- a/server/i18n/scripts/utils.ts +++ b/server/i18n/scripts/utils.ts @@ -72,16 +72,38 @@ function flattenLocalisationRecursive( } } +const UNSAFE_LOCALISATION_KEYS = new Set([ + "__proto__", + "constructor", + "prototype", +]); + +/** Rejects key segments that could traverse the prototype chain. */ +function assertSafeKeyPart(part: string): void { + if (UNSAFE_LOCALISATION_KEYS.has(part)) { + throw new Error(`Unsafe localisation key segment: ${part}`); + } +} + +/** Own-property check that never consults the prototype chain. */ +function hasOwn(target: object, key: string): boolean { + return Object.prototype.hasOwnProperty.call(target, key); +} + export function deleteLocalisation(localisation: Localisation, key: string) { const parts = key.split("."); let current: Localisation | string = localisation; for (const part of parts.slice(0, -1)) { - if (typeof current === "string") + assertSafeKeyPart(part); + if (typeof current === "string" || !hasOwn(current, part)) throw new Error(`${key} not found in localisation`); current = current[part]; } if (typeof current === "string") throw new Error(`${key} not found in localisation`); + assertSafeKeyPart(parts.at(-1)!); + if (!hasOwn(current, parts.at(-1)!)) + throw new Error(`${key} not found in localisation`); // eslint-disable-next-line @typescript-eslint/no-dynamic-delete delete current[parts.at(-1)!]; @@ -94,12 +116,14 @@ export function fetchLocalisation( const parts = key.split("."); let current: Localisation | string = localisation; for (const part of parts.slice(0, -1)) { - if (typeof current === "string") + assertSafeKeyPart(part); + if (typeof current === "string" || !hasOwn(current, part)) throw new Error(`${key} not found in localisation`); current = current[part]; } if (typeof current === "string") throw new Error(`${key} not found in localisation`); + assertSafeKeyPart(parts.at(-1)!); return current[parts.at(-1)!] as string; } diff --git a/server/server/internal/plugins/__tests__/plugins.test.ts b/server/server/internal/plugins/__tests__/plugins.test.ts index 75d93d560..ded25950a 100644 --- a/server/server/internal/plugins/__tests__/plugins.test.ts +++ b/server/server/internal/plugins/__tests__/plugins.test.ts @@ -1608,3 +1608,44 @@ test("PluginManager registers and gates MetadataProvider, CloudSavePathResolver, assert.equal(manager.getPaymentGateways().length, 0); assert.equal(manager.getPaymentGateway("stripe"), undefined); }); + +test("plugin route patterns keep regex metacharacters literal", async () => { + const manager = createTestManager(); + + const regexPlugin: ServerPlugin = { + metadata: { + id: "regex-plugin", + name: "Regex Plugin", + version: "1.0.0", + apiVersion: PLUGIN_API_VERSION, + capabilities: ["routes"], + }, + init: (ctx: PluginContext) => { + ctx.registerRoute("GET", "/literal/(a+)+", () => ({ literal: true })); + }, + }; + + await manager.registerPlugin(regexPlugin); + + const mockEvent = { + method: "GET", + headers: new Headers(), + } as unknown as import("h3").H3Event; + + // The exact literal path matches... + const literal = (await manager.dispatch( + "regex-plugin", + "GET", + "/literal/(a+)+", + mockEvent, + )) as { literal: boolean }; + assert.equal(literal.literal, true); + + // ...but a path the metacharacters would match if compiled as a regex does not. + await assert.rejects( + () => manager.dispatch("regex-plugin", "GET", "/literal/aaaa", mockEvent), + /No handler found/, + ); + + await manager.unregisterPlugin("regex-plugin"); +}); diff --git a/server/server/internal/plugins/manager.ts b/server/server/internal/plugins/manager.ts index 0348b50ca..b479a2831 100644 --- a/server/server/internal/plugins/manager.ts +++ b/server/server/internal/plugins/manager.ts @@ -112,6 +112,45 @@ function trimTrailingSlashes(value: string): string { return value.slice(0, end); } +/** Upper bound on a plugin route pattern, limiting regex construction cost. */ +const MAX_ROUTE_PATTERN_LENGTH = 512; + +/** Escapes RegExp metacharacters so literal route text cannot form a pattern. */ +function escapeRegExp(value: string): string { + const specials = ".*+?^${}()|[]\\"; + let escaped = ""; + for (const char of value) { + escaped += specials.includes(char) ? `\\${char}` : char; + } + return escaped; +} + +/** + * Translates one token of a plugin route pattern into regex source. Recognises + * `:name`, `*`, and `**`; every other character is escaped so it stays literal. + */ +function routeTokenToRegexSource( + normalized: string, + index: number, + paramNames: string[], +): { source: string; consumed: number } { + const char = normalized[index]; + if (char === ":" && /[A-Za-z0-9_]/.test(normalized[index + 1] ?? "")) { + let end = index + 1; + while (end < normalized.length && /\w/.test(normalized[end])) { + end++; + } + paramNames.push(normalized.slice(index + 1, end)); + return { source: "([^/]+)", consumed: end - index }; + } + if (char === "*") { + return normalized[index + 1] === "*" + ? { source: "(.*)", consumed: 2 } + : { source: "([^/]+)", consumed: 1 }; + } + return { source: escapeRegExp(char), consumed: 1 }; +} + /** Resolves a dynamically imported bundle's default or named plugin export. */ function resolvePluginExport(mod: { default?: unknown; @@ -219,19 +258,28 @@ export class PluginManager { regex: RegExp; paramNames: string[]; } { + if (pattern.length > MAX_ROUTE_PATTERN_LENGTH) { + throw new Error( + `Plugin route pattern exceeds ${MAX_ROUTE_PATTERN_LENGTH} characters`, + ); + } const paramNames: string[] = []; - const normalized = pattern.startsWith("/") ? pattern : `/${pattern}`; + const normalized = trimTrailingSlashes( + pattern.startsWith("/") ? pattern : `/${pattern}`, + ); - const regexStr = trimTrailingSlashes(normalized) - .replace(/:(\w+)/g, (_, name) => { - paramNames.push(name); - return "([^/]+)"; - }) - .replace(/\*\*/g, "(.*)") - .replace(/\*/g, "([^/]+)"); + // Build the pattern token-by-token and escape every literal character, so + // a plugin cannot inject regex metacharacters (and thus a ReDoS payload) + // through a route pattern. + let source = ""; + for (let index = 0; index < normalized.length;) { + const token = routeTokenToRegexSource(normalized, index, paramNames); + source += token.source; + index += token.consumed; + } return { - regex: new RegExp(`^${regexStr || "/"}(?:/)?$`), + regex: new RegExp(`^${source || "/"}(?:/)?$`), paramNames, }; }