diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..feb055c --- /dev/null +++ b/.env.example @@ -0,0 +1,31 @@ +# Copy to .env. Everything in the first section is needed to run in production. +# Optional features are below it, switched off until you uncomment them. + +# ============ REQUIRED IN PRODUCTION ============ + +# Cloudflare Tunnel token (Zero Trust dashboard) +TUNNEL_TOKEN=your-tunnel-token + +# Your site's address. The API won't start without it. +ALLOWED_ORIGIN=https://finkit.example.com + +# Cloudflare Access sign-in. The API won't start without both. +CF_ACCESS_TEAM_DOMAIN=yourteam.cloudflareaccess.com +CF_ACCESS_AUD=audience-tag-of-the-access-application + +# Who may use FinKit (comma separated). The first address inherits any existing data. +ALLOWED_USER_EMAILS=you@example.com + +# ============ OPTIONAL ============ + +# --- Bank sync (Enable Banking) --- +# Set all of these together. More detail: api/.env.example +# BANKING_PROVIDER=enablebanking +# BANKING_ENCRYPTION_KEY= # openssl rand -base64 32 +# BANKING_REDIRECT_URL=https://finkit.example.com/api/banking/callback # https, registered in your Enable Banking app +# EB_APP_ID= +# EB_PRIVATE_KEY_PATH=/run/secrets/enablebanking.pem # put the file at ./secrets/enablebanking.pem +# BANKING_AUTO_SYNC_HOURS=12 # scheduled catch-up; minimum 6, 0 = off + +# --- Google Sheets export --- +# GOOGLE_SERVICE_ACCOUNT_KEY_PATH=/run/secrets/google-sa.json # put the file at ./secrets/google-sa.json diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 1ee4f09..319a852 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -13,11 +13,45 @@ permissions: packages: write jobs: + # ============================================ + # Lint, types and tests: nothing is built until these pass + # ============================================ + check: + name: Check (${{ matrix.app }}) + runs-on: ubuntu-latest + timeout-minutes: 15 + strategy: + fail-fast: false + matrix: + app: [api, web] + defaults: + run: + working-directory: ${{ matrix.app }} + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version-file: .nvmrc + cache: npm + cache-dependency-path: ${{ matrix.app }}/package-lock.json + + - run: npm ci + - name: Lint and format + run: npm run check + - name: Types (web; the API check already includes them) + if: matrix.app == 'web' + run: npx tsc --noEmit -p . + - name: Tests + run: npm test + # ============================================ # API Dependencies (cached node_modules) # ============================================ api-deps: name: API deps (cached node_modules) + needs: [check] + if: github.event_name != 'pull_request' runs-on: ubuntu-24.04-arm timeout-minutes: 20 steps: @@ -38,20 +72,6 @@ jobs: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - - name: Build and push deps (builder) - uses: docker/build-push-action@v6 - with: - context: ./api - file: ./api/Dockerfile.deps - target: deps-builder - platforms: linux/arm64 - push: ${{ github.event_name != 'pull_request' }} - tags: | - ghcr.io/${{ steps.owner.outputs.owner_lc }}/finkit-api-deps:builder - ghcr.io/${{ steps.owner.outputs.owner_lc }}/finkit-api-deps:builder-sha-${{ github.sha }} - cache-from: type=gha,scope=api-deps-builder - cache-to: type=gha,mode=max,scope=api-deps-builder - - name: Build and push deps (production) uses: docker/build-push-action@v6 with: @@ -61,8 +81,8 @@ jobs: platforms: linux/arm64 push: ${{ github.event_name != 'pull_request' }} tags: | - ghcr.io/${{ steps.owner.outputs.owner_lc }}/finkit-api-deps:production ghcr.io/${{ steps.owner.outputs.owner_lc }}/finkit-api-deps:production-sha-${{ github.sha }} + ghcr.io/${{ steps.owner.outputs.owner_lc }}/finkit-api-deps:production cache-from: type=gha,scope=api-deps-production cache-to: type=gha,mode=max,scope=api-deps-production @@ -72,7 +92,9 @@ jobs: api: name: API app runs-on: ubuntu-24.04-arm - needs: [api-deps] + needs: [check, api-deps] + # api-deps doesn't run on pull requests (nothing is published there); the build installs its own + if: ${{ !cancelled() && needs.check.result == 'success' && contains(fromJSON('["success","skipped"]'), needs.api-deps.result) }} timeout-minutes: 10 steps: - uses: actions/checkout@v4 @@ -103,6 +125,18 @@ jobs: type=semver,pattern={{version}} type=sha,prefix=sha-,format=short + # Pull requests publish nothing, so they build their own production dependencies (api/Dockerfile). + - name: Dependency image + id: deps + run: | + { + echo 'args<> "$GITHUB_OUTPUT" + - name: Build and push uses: docker/build-push-action@v6 with: @@ -112,9 +146,7 @@ jobs: push: ${{ github.event_name != 'pull_request' }} tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} - build-args: | - BUILDER_DEPS_IMAGE=ghcr.io/${{ steps.owner.outputs.owner_lc }}/finkit-api-deps:builder - PRODUCTION_DEPS_IMAGE=ghcr.io/${{ steps.owner.outputs.owner_lc }}/finkit-api-deps:production + build-args: ${{ steps.deps.outputs.args }} cache-from: type=gha,scope=api-app cache-to: type=gha,mode=max,scope=api-app @@ -123,6 +155,7 @@ jobs: # ============================================ web: name: Web app + needs: [check] runs-on: ubuntu-24.04-arm timeout-minutes: 10 steps: diff --git a/.gitignore b/.gitignore index b8e04db..70b4441 100644 --- a/.gitignore +++ b/.gitignore @@ -56,6 +56,7 @@ temp/ # Web specific web/.bolt/ +web/tsconfig.tsbuildinfo # Database files (SQLite) *.db @@ -75,3 +76,10 @@ data/ *.pem secrets/ .finkit-secrets/ + +# Real bank exports used to test the Enable Banking mock. Never commit. +tilisy*.json +**/mock-upload*.json + +# Playwright MCP scratch output +.playwright-mcp/ diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..8cde87d --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +26.13.0 \ No newline at end of file diff --git a/README-v2.md b/README-v2.md deleted file mode 100644 index f49068f..0000000 --- a/README-v2.md +++ /dev/null @@ -1,135 +0,0 @@ -# FinKit - -A privacy-first transaction analysis tool that anonymizes your bank data before processing and exports to your Google Sheets yearly breakdown. - -## Features - -- **Privacy-First**: All personal data is anonymized before any processing -- **Smart Categorization**: Rule-based categorization with AI fallback -- **Recurring Detection**: Automatically identifies recurring transactions -- **Inline Editing**: Click any category to change it -- **Bulk Operations**: Select multiple transactions and recategorize at once -- **Advanced Analytics**: Charts, trends, and spending breakdowns -- **Google Sheets Export**: One-click export to your existing yearly breakdown - -## Quick Start - -### 1. Install Dependencies - -```bash -# Install web dependencies -cd web-v2 -npm install - -# Install API dependencies -cd ../api-v2 -npm install -``` - -### 2. Configure Environment - -Create `.env` file in `api-v2/`: - -```env -OPENAI_API_KEY=your-openai-api-key -GOOGLE_SERVICE_ACCOUNT_KEY_PATH=/path/to/service-account.json -``` - -### 3. Run Development Servers - -In separate terminals: - -```bash -# Terminal 1: Run API (runs on port 3003) -cd api-v2 -npm run dev - -# Terminal 2: Run Web (runs on port 5173, proxies /api to 3003) -cd web-v2 -npm run dev -``` - -Open http://localhost:5173 - -## Project Structure - -``` -├── web-v2/ # React + Vite frontend -│ ├── src/ -│ │ ├── components/ # UI components -│ │ │ ├── Dashboard.tsx -│ │ │ ├── TransactionTable.tsx -│ │ │ ├── SettingsPanel.tsx -│ │ │ ├── ExportPreview.tsx -│ │ │ └── charts/ -│ │ ├── lib/ # Core logic -│ │ │ ├── anonymizer.ts -│ │ │ ├── parser.ts -│ │ │ ├── categorizer.ts -│ │ │ └── recurring.ts -│ │ └── types.ts -│ └── package.json -│ -├── api-v2/ # Hono API server -│ ├── src/ -│ │ ├── index.ts -│ │ ├── routes/ -│ │ │ ├── categorize.ts -│ │ │ └── export.ts -│ │ ├── services/ -│ │ │ ├── openai.ts -│ │ │ └── sheets.ts -│ │ └── lib/ -│ │ └── anonymizer.ts -│ └── package.json -``` - -## Google Sheets Setup - -### 1. Create Service Account - -1. Go to [Google Cloud Console](https://console.cloud.google.com/) -2. Create a new project or select existing -3. Enable Google Sheets API -4. Create a Service Account -5. Download the JSON key file -6. Share your Google Sheet with the service account email - -### 2. Configure Export Settings - -In the app: -1. Click "Settings" -2. Paste your Google Sheet URL or ID -3. Set the tab name (e.g., "2026") -4. Map your categories to sheet columns - -## Your Sheet Structure - -The app is designed for this column layout: - -| A | B | C | D | E | ... | R | S | T | U | V | W | -|---|---|---|---|---|-----|---|---|---|---|---|---| -| Month | Rent | Eating Out | Personal Entertainment | Subscriptions | ... | Total Expenditure | Income | Income After Expenditure | Gross Savings | Gross Investment | Net Income | - -Configure mappings in Settings to match your exact layout. - -## Anonymization - -The following data is anonymized: - -| Data Type | Anonymization | -|-----------|--------------| -| Names | `Person_ABC` | -| IBANs | `DE**********5515` | -| Account Numbers | `Acc_XYZ123` | -| Emails | `j***@gmail.com` | -| Phone Numbers | `+*********1234` | - -Mappings are kept in memory only and never persisted. - -## Tech Stack - -- **Frontend**: React 18, Vite, TailwindCSS, Recharts -- **Backend**: Hono (lightweight Express alternative) -- **AI**: OpenAI GPT-4o-mini -- **Export**: Google Sheets API diff --git a/README.md b/README.md index 0edc4a6..ae917cd 100644 --- a/README.md +++ b/README.md @@ -1,124 +1,140 @@ # FinKit -A simple tool to parse and visualize transaction records exported from FinanzGuru. +**Self-hosted** personal finance tooling: you run the web app and API on your own machine or server. Import transactions (CSV/XLSX or optional bank sync), categorize them, explore spending, and optionally push summary rows to **your** Google Sheet. -## Features +There is no FinKit cloud, no vendor account, and **no built-in analytics, telemetry, or data collection**. Nothing is sent to the authors of this software unless **you** configure an optional integration below. -- **CSV/XLSX Import**: Parse transaction exports from FinanzGuru -- **Privacy-First**: Anonymizes personal data before processing -- **Smart Categorization**: Rule-based categorization with AI fallback -- **Recurring Detection**: Identifies recurring transactions automatically -- **Interactive Dashboard**: View spending trends, category breakdowns, and top expenses -- **Bulk Editing**: Select and recategorize multiple transactions at once -- **Google Sheets Export**: Export to your existing yearly budget spreadsheet +## Privacy & data (short version) -## Quick Start +| Where (all on **your** deployment) | What is kept | +|--------|----------------| +| **Your browser** | Parsed transactions from **file import** (never uploaded to the API); optional local session (IndexedDB) if you leave “remember session” on. Optional in-memory anonymization mappings (never persisted). | +| **Your self-hosted API** (SQLite on **your** server or laptop) | Per-user merchant rules, settings, encrypted bank connection tokens, and (if bank sync is on) **synced bank transactions** on the server—scoped by signed-in user, with descriptive fields encrypted at rest. Disconnecting a bank or using **Stored bank data** on the Banks page removes server-side sync data. | +| **Nowhere else by default** | No FinKit-operated cloud, no background uploads, crash reports, usage stats, or third-party trackers in the app. | -### Prerequisites +**Optional outbound connections (you enable these):** -- Node.js 18+ -- OpenAI API key (for AI categorization) -- Google Service Account (for Sheets export, optional) +- **Google Sheets** — only when you configure a service account and export from the app; sends the summary row you choose to a spreadsheet you control. +- **Open Banking (Enable Banking)** — only when `BANKING_PROVIDER=enablebanking`; **your** API talks to Enable Banking and your bank. FinKit never sees your bank password. +- **Cloudflare Access** — in production, the recommended gate in front of the app; the API verifies Access JWTs itself. Cloudflare’s privacy terms apply to that layer. -### 1. Setup Environment +See [web/public/privacy.html](./web/public/privacy.html) and [web/public/terms.html](./web/public/terms.html) for the full privacy and terms copy shown on the landing site. -Create `.env` file in the `api/` directory: +## Features -```env -OPENAI_API_KEY=your-openai-api-key -GOOGLE_SERVICE_ACCOUNT_KEY_PATH=/path/to/service-account.json -``` +- **Import**: FinanzGuru CSV/XLSX (client-side); optional Open Banking sync when configured +- **Multi-user (allowlist)**: `ALLOWED_USER_EMAILS` — each person sees only their rules, settings, banks, and synced data ([details](./docs/users-and-access.md)) +- **Categorization**: Client-side rules, recurring detection, and server-stored merchant rules (learned when you recategorize) +- **Review & edit**: Inline categories, bulk actions, income and review queues +- **Analytics**: Trends, category breakdowns, top expenses (computed in the browser from your data) +- **Export**: Optional push of monthly totals to your Google Sheet +- **Privacy**: Optional in-browser anonymization of names, IBANs, emails, and similar fields + +## Prerequisites + +- Node.js 26.13+ (matches Docker `node:26-alpine`; see `.nvmrc`). Nest 12 CLI officially supports the 26.x line. +- For Sheets export: Google Cloud service account with Sheets API enabled +- For production deploy: [.env.example](./.env.example) (tunnel, registry, Google credentials, **Cloudflare Access**, **ALLOWED_ORIGIN**, **ALLOWED_USER_EMAILS**) +- For bank sync: [api/.env.example](./api/.env.example) and [Enable Banking setup](./docs/enable-banking-setup.md) -### 2. Install & Run with Docker +## Local development + +From the repo root (`npm install` once installs root tooling only): ```bash -# Development -docker-compose up +npm run install:all # api + web dependencies +npm run dev # API :3003 and web :5173 together +# or: npm run dev:api | npm run dev:web in separate terminals +``` + +Or per package: -# Production -docker-compose -f docker-compose.prod.yml up -d +```bash +cd api && npm install && npm run dev # http://localhost:3003 +cd web && npm install && npm run dev # http://localhost:5173 — proxies /api → :3003 ``` -The web app will be available at `http://localhost:5173` (dev) or `http://localhost` (production). +Open **http://localhost:5173/app/** for the app (landing page is at `/`). + +Copy [api/.env.example](./api/.env.example) to `api/.env`. Defaults are enough for local import, categorization, and merchant rules (SQLite under `api/data`). Locally you are `dev@localhost`; use header `X-Dev-User: other@example.com` to simulate another allowlisted user (ignored in production). -### 3. Manual Setup (without Docker) +### Tests and quality ```bash -# Install API dependencies -cd api -npm install +npm test # api + web +npm run check # oxlint + oxfmt --check in both packages +npm run lint:fix && npm run format # auto-fix style (run format after lint:fix) +npm run build # production build both packages +``` -# Install Web dependencies -cd ../web -npm install +## Documentation -# Run both (in separate terminals) -cd api && npm run dev # API runs on :3003 -cd web && npm run dev # Web runs on :5173 -``` +- [Users and access](./docs/users-and-access.md) — Cloudflare Access, allowlist, per-user data, production env requirements +- [Web frontend architecture](./docs/web-architecture.md) — feature layout, AppState, conventions +- [Enable Banking setup](./docs/enable-banking-setup.md) — optional open banking configuration -## Usage +## Production (Docker) -1. **Export from FinanzGuru**: Export your transactions as CSV or XLSX -2. **Upload**: Drag and drop the file into the web interface -3. **Review**: Check auto-categorized transactions, edit as needed -4. **Analyze**: View charts and spending breakdowns -5. **Export**: (Optional) Export to Google Sheets +You host the containers; data stays in volumes you control. -## Project Structure +1. Copy [.env.example](./.env.example) to `.env` and set at least **`ALLOWED_USER_EMAILS`**, **`CF_ACCESS_TEAM_DOMAIN`**, **`CF_ACCESS_AUD`**, and **`ALLOWED_ORIGIN`** (the API **will not start** in production without Cloudflare Access config and a fixed web origin). See [users and access](./docs/users-and-access.md). +2. Deploy: +```bash +docker compose -f docker-compose.prod.yml pull +docker compose -f docker-compose.prod.yml up -d ``` -├── api/ # Backend API (Hono) -│ ├── src/ -│ │ ├── routes/ # API endpoints -│ │ ├── services/ # OpenAI, Sheets integration -│ │ └── lib/ # Anonymization logic -│ └── Dockerfile -│ -├── web/ # Frontend (React + Vite) -│ ├── src/ -│ │ ├── components/ # UI components -│ │ ├── lib/ # Parsers, categorization -│ │ └── types.ts -│ └── Dockerfile -│ -└── docker-compose.yml # Development setup + +### Key files (only for bank sync or Google Sheets) + +The `.env` values `EB_PRIVATE_KEY_PATH` and `GOOGLE_SERVICE_ACCOUNT_KEY_PATH` are paths **inside** the API container. The compose file mounts the `secrets/` folder next to it, read-only, at `/run/secrets`, so a file at `secrets/enablebanking.pem` on your server is `/run/secrets/enablebanking.pem` for the API. + +```bash +[ -e secrets/enablebanking.pem ] && echo "already exists, not overwriting" || { + mkdir -p secrets && + openssl genrsa -out secrets/enablebanking.pem 4096 2>/dev/null && + openssl req -new -x509 -days 1095 -key secrets/enablebanking.pem -out secrets/enablebanking.crt -subj "/C=DE/O=FinKit/CN=finkit" && + chmod 644 secrets/enablebanking.pem secrets/enablebanking.crt && + cat secrets/enablebanking.crt +} ``` -## Data Privacy +Run it from the folder with `docker-compose.prod.yml`. It creates `secrets/`, generates the Enable Banking signing key there, and prints the certificate to paste into the Enable Banking form. The private key never leaves the server. For Sheets export, save the JSON key Google gives you as `secrets/google-sa.json`. The API runs as a non-root user, hence the `644`. + +Then set `EB_PRIVATE_KEY_PATH=/run/secrets/enablebanking.pem` and/or `GOOGLE_SERVICE_ACCOUNT_KEY_PATH=/run/secrets/google-sa.json` in `.env` and restart. The folder is gitignored; copy the files to the server yourself. -All sensitive data is anonymized before processing: +API health: `GET /health`. Persistent SQLite data uses the `finkit-api-data` volume on **your** host. -- **Names**: `Person_ABC` -- **IBANs**: `DE**********5515` -- **Account Numbers**: `Acc_XYZ123` -- **Emails**: `j***@gmail.com` -- **Phone Numbers**: `+*********1234` +Root [docker-compose.yml](./docker-compose.yml) builds the same web and API images for local or CI use; wire ports or a reverse proxy so the browser can reach the web container and `/api` can reach the API. -Anonymization mappings are kept in memory only and never persisted. +## Google Sheets (optional) -## Google Sheets Setup (Optional) +Only runs when you set credentials and export from the app: -### 1. Create Service Account +1. In [Google Cloud Console](https://console.cloud.google.com/), enable the Sheets API and create a service account. +2. Share **only** the spreadsheet FinKit should use with the service account email (Editor). Export calls pass a spreadsheet id from the app; **narrow sharing on the SA** is what limits where it can write ([users and access — Sheets](./docs/users-and-access.md#google-sheets-export)). +3. **Local API**: `GOOGLE_SERVICE_ACCOUNT_KEY_PATH` in `api/.env`. +4. **Docker production**: mount the JSON key as a secret and set `GOOGLE_SERVICE_ACCOUNT_KEY_PATH` in the root `.env` (see [.env.example](./.env.example)). +5. In **Settings**, set sheet URL/ID, tab name, and category column mappings. -1. Go to [Google Cloud Console](https://console.cloud.google.com/) -2. Enable Google Sheets API -3. Create a Service Account and download JSON key -4. Share your Google Sheet with the service account email +Tab names and column letters are validated before any Google API call. The export expects a monthly summary layout (month row plus category columns and totals). -### 2. Configure in App +## Project layout -1. Open Settings panel -2. Enter your Google Sheet URL or ID -3. Set the target tab name (e.g., "2026") -4. Map categories to sheet columns +``` +package.json Root scripts: dev, build, test, lint, format, check (both packages) +api/ NestJS API — access control, per-user rules/settings, Sheets export, optional banking +web/ React + Vite SPA (see docs/web-architecture.md) +docs/ Architecture and integration guides +.env.example Production compose and secrets template +``` -## Tech Stack +## Tech stack -- **Frontend**: React 18, Vite, TailwindCSS, Recharts -- **Backend**: Hono (Node.js) -- **AI**: OpenAI GPT-4o-mini -- **Export**: Google Sheets API +- **Web**: React 18, Vite, TanStack Router, TanStack Query, TailwindCSS, Recharts +- **API**: NestJS, SQLite (better-sqlite3), Zod, oxlint/oxfmt +- **Export**: Google Sheets API (optional) +- **Auth (production)**: Cloudflare Access JWT verification on the API ## License diff --git a/api/.env.example b/api/.env.example new file mode 100644 index 0000000..dea0266 --- /dev/null +++ b/api/.env.example @@ -0,0 +1,28 @@ +# Copy to .env for local runs. In production these go in the root .env (see ../.env.example). + +PORT=3000 +# DATA_DIR=./data +# Browser origin allowed to make changes. When set, changing requests must send exactly this Origin. +# Leave unset locally. REQUIRED in production: the API won't start without it. +# ALLOWED_ORIGIN=https://finkit.example.com + +# Who may use FinKit, comma separated. Each person sees only their own data. +# The first address also inherits any data from before accounts existed. +# Required in production: with none listed, nobody gets in. +# ALLOWED_USER_EMAILS=you@example.com + +# Cloudflare Access (required in production: the server won't start without both) +# CF_ACCESS_TEAM_DOMAIN=yourteam.cloudflareaccess.com +# CF_ACCESS_AUD= + +# Bank sync: off | mock (local test bank) | enablebanking +BANKING_PROVIDER=off +# Hours between scheduled catch-up syncs (default 12, minimum 6, 0 = off). Banks allow only a few unattended reads a day. +# BANKING_AUTO_SYNC_HOURS=12 +# BANKING_ENCRYPTION_KEY= # openssl rand -base64 32; required in production +# BANKING_REDIRECT_URL=https://finkit.example.com/api/banking/callback +# EB_APP_ID= +# EB_PRIVATE_KEY_PATH=/run/secrets/enablebanking.pem + +# Google Sheets (optional) +# GOOGLE_SERVICE_ACCOUNT_KEY_PATH=/run/secrets/google-sa.json diff --git a/api/.oxfmtrc.json b/api/.oxfmtrc.json new file mode 100644 index 0000000..bbe877c --- /dev/null +++ b/api/.oxfmtrc.json @@ -0,0 +1,9 @@ +{ + "$schema": "./node_modules/oxfmt/configuration_schema.json", + "printWidth": 120, + "semi": false, + "singleQuote": true, + "trailingComma": "all", + "arrowParens": "avoid", + "ignorePatterns": ["dist", "node_modules", "data", "package-lock.json"] +} diff --git a/api/.oxlintrc.json b/api/.oxlintrc.json new file mode 100644 index 0000000..fe81220 --- /dev/null +++ b/api/.oxlintrc.json @@ -0,0 +1,51 @@ +{ + "$schema": "./node_modules/oxlint/configuration_schema.json", + "jsPlugins": ["@stylistic/eslint-plugin"], + "plugins": ["typescript", "unicorn", "oxc", "import", "node", "promise", "vitest"], + "categories": { + "correctness": "error", + "suspicious": "warn" + }, + "rules": { + "curly": ["error", "all"], + "@stylistic/max-statements-per-line": ["error", { "max": 1 }], + "@stylistic/padding-line-between-statements": [ + "error", + { "blankLine": "always", "prev": "*", "next": "return" }, + { "blankLine": "always", "prev": ["const", "let", "var"], "next": "*" }, + { "blankLine": "any", "prev": ["const", "let", "var"], "next": ["const", "let", "var"] } + ], + "@stylistic/lines-between-class-members": [ + "error", + { + "enforce": [{ "blankLine": "always", "prev": "*", "next": "method" }] + } + ], + "typescript/no-extraneous-class": "off", + "vitest/expect-expect": [ + "error", + { + "assertFunctionNames": ["expect", "request.**.expect"] + } + ] + }, + "env": { + "builtin": true, + "node": true + }, + "ignorePatterns": ["dist", "node_modules", "data"], + "overrides": [ + { + "files": ["src/main.ts"], + "rules": { + "import/no-unassigned-import": "off" + } + }, + { + "files": ["**/*.test.ts"], + "rules": { + "unicorn/consistent-function-scoping": "off" + } + } + ] +} diff --git a/api/Dockerfile b/api/Dockerfile index 90494ba..84b0733 100644 --- a/api/Dockerfile +++ b/api/Dockerfile @@ -1,31 +1,42 @@ -# Build stage - uses cached dependencies from Dockerfile.deps -ARG BUILDER_DEPS_IMAGE=builder-deps:latest -ARG PRODUCTION_DEPS_IMAGE=production-deps:latest +# Production node_modules come from the published Dockerfile.deps image when CI passes one (main +# and tags). With no build arg, such as in a pull request or a plain `docker build`, they are +# installed right here, so a PR needs to publish nothing and never uses a stale image. +ARG PRODUCTION_DEPS_IMAGE=local-deps-production + +FROM node:26-alpine AS local-deps-production +WORKDIR /app +RUN apk add --no-cache python3 make g++ git +COPY package*.json ./ +COPY scripts ./scripts +RUN npm ci --omit=dev +RUN test -f node_modules/better-sqlite3/build/Release/better_sqlite3.node || \ + (echo "ERROR: better-sqlite3 binary not found" && exit 1) -FROM ${BUILDER_DEPS_IMAGE} AS deps-builder FROM ${PRODUCTION_DEPS_IMAGE} AS deps-production # ============================================ -# Builder stage +# Builder stage (devDependencies + compile; do not reuse production-only deps image) # ============================================ -FROM node:20-alpine AS builder +FROM node:26-alpine AS builder WORKDIR /app -# Copy cached node_modules from deps-builder -COPY --from=deps-builder /app/node_modules ./node_modules -COPY --from=deps-builder /app/package*.json ./ +RUN apk add --no-cache python3 make g++ + +COPY package*.json ./ +COPY scripts ./scripts + +RUN --mount=type=cache,target=/root/.npm \ + npm ci -# Copy source code COPY . . -# Build TypeScript RUN npm run build # ============================================ # Production stage # ============================================ -FROM node:20-alpine AS production +FROM node:26-alpine AS production WORKDIR /app @@ -59,4 +70,4 @@ EXPOSE 3000 HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \ CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1 -CMD ["node", "dist/index.js"] +CMD ["node", "dist/main.js"] diff --git a/api/Dockerfile.deps b/api/Dockerfile.deps index 5fae729..a97b94c 100644 --- a/api/Dockerfile.deps +++ b/api/Dockerfile.deps @@ -1,7 +1,7 @@ # Dockerfile.deps - Dependency caching layer for faster builds # This creates reusable dependency images that only rebuild when package files change -FROM node:20-alpine AS deps-base +FROM node:26-alpine AS deps-base WORKDIR /app diff --git a/api/nest-cli.json b/api/nest-cli.json new file mode 100644 index 0000000..20ffa73 --- /dev/null +++ b/api/nest-cli.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://json.schemastore.org/nest-cli", + "collection": "@nestjs/schematics", + "sourceRoot": "src", + "compilerOptions": { + "builder": "swc", + "typeCheck": true, + "tsConfigPath": "tsconfig.build.json", + "deleteOutDir": true + } +} diff --git a/api/package-lock.json b/api/package-lock.json index 2e8bd45..8fd3b24 100644 --- a/api/package-lock.json +++ b/api/package-lock.json @@ -9,473 +9,718 @@ "version": "2.0.0", "hasInstallScript": true, "dependencies": { - "@hono/node-server": "^2.1.5", - "better-sqlite3": "12.8.0", - "googleapis": "^189.0.0", - "hono": "^4.13.16", - "openai": "^7.47.0", - "zod": "^4.6.7" + "@nestjs/common": "12.3.2", + "@nestjs/core": "12.3.2", + "@nestjs/platform-express": "12.3.2", + "better-sqlite3": "12.11.1", + "googleapis": "189.0.0", + "helmet": "8.3.0", + "jose": "5.10.0", + "reflect-metadata": "0.2.2", + "rxjs": "7.8.2", + "zod": "4.6.7" }, "devDependencies": { - "@types/better-sqlite3": "^7.6.13", - "@types/node": "^22.10.0", - "tsup": "^8.5.1", - "tsx": "^4.23.15", - "typescript": "~5.6.2" + "@nestjs/cli": "12.0.11", + "@nestjs/testing": "12.3.2", + "@stylistic/eslint-plugin": "4.4.1", + "@swc/cli": "0.8.2", + "@swc/core": "1.16.21", + "@types/better-sqlite3": "7.6.13", + "@types/express": "5.0.6", + "@types/node": "24.19.2", + "@types/supertest": "7.2.1", + "oxfmt": "0.75.0", + "oxlint": "1.90.0", + "supertest": "7.3.1", + "typescript": "6.0.3", + "unplugin-swc": "2.0.0", + "vitest": "3.2.7" + }, + "engines": { + "node": ">=26.13.0" } }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", - "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", - "cpu": [ - "ppc64" - ], + "node_modules/@angular-devkit/core": { + "version": "22.2.3", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-22.2.3.tgz", + "integrity": "sha512-bi1oBjJeg1XCgRrBYSuJikaJV4sO/mcnyFE/0a1H646yHhQpVYoU/M2Vh69Qv8h+YoMfC7RuSxT4LZHaubhhLw==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "aix" - ], + "dependencies": { + "ajv": "8.20.0", + "ajv-formats": "3.0.1", + "jsonc-parser": "3.3.1", + "picomatch": "4.0.7", + "rxjs": "7.8.2", + "source-map": "0.8.0" + }, "engines": { - "node": ">=18" + "node": "^22.22.3 || ^24.15.0 || >=26.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^5.0.0" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } } }, - "node_modules/@esbuild/android-arm": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", - "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", - "cpu": [ - "arm" - ], + "node_modules/@angular-devkit/schematics": { + "version": "22.2.3", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-22.2.3.tgz", + "integrity": "sha512-+XfBViIGqiFQK8KJXA6PgANX9tLuMO/x8GFjlzEmA4LIe9ikAtpGZw+Ezo547BIJQoJBIKCJYR0knE4BmbFeSQ==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "android" - ], + "dependencies": { + "@angular-devkit/core": "22.2.3", + "jsonc-parser": "3.3.1", + "magic-string": "1.4.1", + "ora": "9.4.1", + "rxjs": "7.8.2" + }, "engines": { - "node": ">=18" + "node": "^22.22.3 || ^24.15.0 || >=26.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" } }, - "node_modules/@esbuild/android-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", - "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", - "cpu": [ - "arm64" - ], + "node_modules/@angular-devkit/schematics-cli": { + "version": "22.2.3", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics-cli/-/schematics-cli-22.2.3.tgz", + "integrity": "sha512-GebRkurUplPBwJOz/LT40ZZ0Hxv0EzpllJD1Oj7Bjkk/9A8PKGPDSRgEGfWcVk2h35V4JNNC0g6pSMj9s0dWxg==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "android" - ], + "dependencies": { + "@angular-devkit/core": "22.2.3", + "@angular-devkit/schematics": "22.2.3", + "@inquirer/prompts": "8.7.2" + }, + "bin": { + "schematics": "bin/schematics.js" + }, "engines": { - "node": ">=18" + "node": "^22.22.3 || ^24.15.0 || >=26.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" } }, - "node_modules/@esbuild/android-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", - "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", - "cpu": [ - "x64" - ], + "node_modules/@angular-devkit/schematics-cli/node_modules/@inquirer/prompts": { + "version": "8.7.2", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-8.7.2.tgz", + "integrity": "sha512-QoRB4wFIjgH5iOhSjoIKMkTvSHDuV+O3OITlIqAYO0oK5x364GJILXiMBvlPiE+klg7Xx9tq5XVqQHcGUDYYPA==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "android" - ], + "dependencies": { + "@inquirer/checkbox": "^5.2.5", + "@inquirer/confirm": "^6.3.2", + "@inquirer/editor": "^5.3.3", + "@inquirer/expand": "^5.1.5", + "@inquirer/input": "^5.1.6", + "@inquirer/number": "^4.2.3", + "@inquirer/password": "^5.2.2", + "@inquirer/rawlist": "^5.3.5", + "@inquirer/search": "^4.3.3", + "@inquirer/select": "^5.2.5" + }, "engines": { - "node": ">=18" + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", - "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", - "cpu": [ - "arm64" - ], + "node_modules/@angular-devkit/schematics/node_modules/magic-string": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.4.1.tgz", + "integrity": "sha512-8lyCu36ErXR0J9uaGKlKQoiLZKmtI63YGLE8G2o9jyRPdr4X47LusSOwgOJOzcVtp81fTAAjxR7BwKz682Jhow==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.6.0" } }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", - "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", - "cpu": [ - "x64" - ], + "node_modules/@borewit/text-codec": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@borewit/text-codec/-/text-codec-0.2.3.tgz", + "integrity": "sha512-49CycTz1aAZmvFcwkeFyhud1HUmRIB5Tabf7ALV+VA4Iu85MTfz2cUwzPagMFAdVBvrWdkCxG67I30tUwwpKsw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@cacheable/memory": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@cacheable/memory/-/memory-2.2.0.tgz", + "integrity": "sha512-CTLKqLItRCEixEAewD3/j9DB3/o96gpTPD4eJ1v+DGOlxZRZncRQkGYqqnAGCscYd6RNeXfGeiuCphsPtqyIfQ==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" + "peer": true, + "dependencies": { + "@cacheable/utils": "^2.5.0", + "@keyv/bigmap": "^1.3.1", + "hookified": "^1.15.1", + "keyv": "^5.6.0" } }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", - "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", - "cpu": [ - "arm64" - ], + "node_modules/@cacheable/utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@cacheable/utils/-/utils-2.5.0.tgz", + "integrity": "sha512-buipgOVDkkPXNR5+xBpDw7Zk2n1EvU7qBJCNUcL7rhQ//kfpOXPAvQ511Os0vpLYJ1pZnvudNytkQt2hst3wqA==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" + "peer": true, + "dependencies": { + "hashery": "^1.5.1", + "keyv": "^5.6.0" } }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", - "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", - "cpu": [ - "x64" - ], + "node_modules/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", "dev": true, "license": "MIT", "optional": true, - "os": [ - "freebsd" - ], "engines": { - "node": ">=18" + "node": ">=0.1.90" } }, - "node_modules/@esbuild/linux-arm": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", - "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", - "cpu": [ - "arm" - ], + "node_modules/@eslint-community/eslint-utils": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", + "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, "engines": { - "node": ">=18" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", - "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", - "cpu": [ - "arm64" - ], + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "license": "Apache-2.0", "engines": { - "node": ">=18" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", - "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", - "cpu": [ - "ia32" - ], + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "peer": true, "engines": { - "node": ">=18" + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", - "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", - "cpu": [ - "loong64" - ], + "node_modules/@eslint/config-array": { + "version": "0.23.5", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", + "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@eslint/object-schema": "^3.0.5", + "debug": "^4.3.1", + "minimatch": "^10.2.4" + }, "engines": { - "node": ">=18" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", - "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", - "cpu": [ - "mips64el" - ], + "node_modules/@eslint/config-helpers": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.7.0.tgz", + "integrity": "sha512-DObd/KKUsU+FaFv4PLxSRenpXfQWmPXXP3pPZ6/K1PCrMu2vQpMDMuQe/BqYeoLcz8ro0bVDF1RxOJgfVEdhUw==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@eslint/core": "^1.2.1" + }, "engines": { - "node": ">=18" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", - "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", - "cpu": [ - "ppc64" - ], + "node_modules/@eslint/core": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@types/json-schema": "^7.0.15" + }, "engines": { - "node": ">=18" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", - "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", - "cpu": [ - "riscv64" - ], + "node_modules/@eslint/object-schema": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", + "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "license": "Apache-2.0", + "peer": true, "engines": { - "node": ">=18" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", - "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", - "cpu": [ - "s390x" - ], + "node_modules/@eslint/plugin-kit": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.3.tgz", + "integrity": "sha512-IkO+/KEUvwbVpiURZg+P7zF74z5Jxe0UgJxVni+RtoHQ6IZieXaO02kmadomap/q+l6bc/jdPGGqTjhuZnuz1Q==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@eslint/core": "^1.2.1", + "levn": "^0.4.1" + }, "engines": { - "node": ">=18" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, - "node_modules/@esbuild/linux-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", - "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", - "cpu": [ - "x64" - ], + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@humanfs/types": "^0.15.0" + }, "engines": { - "node": ">=18" + "node": ">=18.18.0" } }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", - "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", - "cpu": [ - "arm64" - ], + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, "engines": { - "node": ">=18" + "node": ">=18.18.0" } }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", - "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", - "cpu": [ - "x64" - ], + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], + "license": "Apache-2.0", + "peer": true, "engines": { - "node": ">=18" + "node": ">=18.18.0" } }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", - "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", - "cpu": [ - "arm64" - ], + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], + "license": "Apache-2.0", + "peer": true, "engines": { - "node": ">=18" + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", - "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", - "cpu": [ - "x64" - ], + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], + "license": "Apache-2.0", + "peer": true, "engines": { - "node": ">=18" + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", - "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", - "cpu": [ - "arm64" - ], + "node_modules/@inquirer/ansi": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-2.0.10.tgz", + "integrity": "sha512-dwrc+1aqVZpHNkNDswXGtn1typ+Pxpo5FHZRyBw1le7onEJUkPUkYOY1FIFC1LvFMYUqqLuWBwElhAmencL7MQ==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], "engines": { - "node": ">=18" + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" } }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", - "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", - "cpu": [ - "x64" - ], + "node_modules/@inquirer/checkbox": { + "version": "5.2.8", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-5.2.8.tgz", + "integrity": "sha512-NY/S1EalQhlonqhwwsarrcnkH7s9ByQuvjWYdk1jB2Gb9EPQJjKkhCTWjM3yQ0UpHaLm1XbIL89b+32G8rhO5w==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], + "dependencies": { + "@inquirer/ansi": "^2.0.10", + "@inquirer/core": "^12.0.6", + "@inquirer/figures": "^2.0.10", + "@inquirer/type": "4.2.1" + }, "engines": { - "node": ">=18" + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", - "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", - "cpu": [ - "arm64" - ], + "node_modules/@inquirer/confirm": { + "version": "6.3.5", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-6.3.5.tgz", + "integrity": "sha512-BJ59BOyURiZiAxdr9AuRu38/k+rV+DcTIM2fUF/c/BXrab6H8zMZTTOl3LgkSJH96Xob2vOJxDiBkHiz32Xx2w==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ], + "dependencies": { + "@inquirer/core": "^12.0.6", + "@inquirer/type": "4.2.1" + }, "engines": { - "node": ">=18" + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", - "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", - "cpu": [ - "ia32" - ], + "node_modules/@inquirer/core": { + "version": "12.0.6", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-12.0.6.tgz", + "integrity": "sha512-5yxn2LsJ8Tu4ei2a+yNkmFRhM/cb+JMEsEs/AW6CCOnBnpMy4IjYiPMFyHtYq8weOmlTyqqCx40y0DsD7OsEPw==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ], + "dependencies": { + "@inquirer/ansi": "^2.0.10", + "@inquirer/figures": "^2.0.10", + "@inquirer/type": "4.2.1", + "cli-width": "^4.1.0", + "fast-wrap-ansi": "^0.2.0", + "mute-stream": "^3.0.0", + "signal-exit": "^4.1.0" + }, "engines": { - "node": ">=18" + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, - "node_modules/@esbuild/win32-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", - "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", - "cpu": [ - "x64" - ], + "node_modules/@inquirer/editor": { + "version": "5.3.6", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-5.3.6.tgz", + "integrity": "sha512-Z5SOeRC5VNkINfKN4rY8BSaYKb3GYi8iSEqZZRqogsE2GhBBToQ0Xw25SbI+4bcatLzU009mZ5d9EuJNiJuUCg==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ], + "dependencies": { + "@inquirer/core": "^12.0.6", + "@inquirer/external-editor": "^3.0.7", + "@inquirer/type": "4.2.1" + }, "engines": { - "node": ">=18" + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, - "node_modules/@hono/node-server": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-2.1.5.tgz", - "integrity": "sha512-MgP/I06GdvsilDspMk/qmRzpk8LOUStP6jFAbSRi40W09DhoyUn+Mj7eyKICPqeYKybOjVFLVIAQzvu1bfIa7Q==", + "node_modules/@inquirer/expand": { + "version": "5.1.8", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-5.1.8.tgz", + "integrity": "sha512-dMEnTkpLqzM/sbILwL7QJgx9iX5hoEM8+bh6shgwd9mYJamdOnth7y2mMaFd08Glj9AXEFKllWcGYyBqnJsHrA==", + "dev": true, "license": "MIT", + "dependencies": { + "@inquirer/core": "^12.0.6", + "@inquirer/type": "4.2.1" + }, "engines": { - "node": ">=20" + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/external-editor": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-3.0.7.tgz", + "integrity": "sha512-enNqTJpojgISCG+a5acjnfJ02hcRTFUBVm8uXiCY40PRPZtSzFnNLskOTU+kDUUEODYma90syxuea1rRtUFh0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "chardet": "^2.1.1", + "iconv-lite": "^0.7.2" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/figures": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-2.0.10.tgz", + "integrity": "sha512-51elrUJV3jZpvD0ygz8+U10Kc6+Lzqln+/tYmnbhy6jo5oXpE6hMCcv+OTBC0enwJXsb3JIuT3VtEinRNCSK2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + } + }, + "node_modules/@inquirer/input": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-5.1.9.tgz", + "integrity": "sha512-2Vd5T7lTxfgJMpAd3aOu6bQpbUnCU7tBgvTQIZkP/7juJzmRmTM2t5XfOCuirFduWLTSJI/WZp32Cq1E01TVeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^12.0.6", + "@inquirer/type": "4.2.1" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/number": { + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-4.2.6.tgz", + "integrity": "sha512-22LnGGCDm1M/pKXxxF8n6ppr/1IztdsxqeqeBRUMTTNnJTkjIBiAkewiDM4u/gjtazj/FvZS+nk9AbO/vEcBFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^12.0.6", + "@inquirer/type": "4.2.1" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/password": { + "version": "5.2.5", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-5.2.5.tgz", + "integrity": "sha512-0v0wLQA4uhWbybmdH6oPl7ooxCiymNylkvi2gCSc+mA5ZQB6QoJLVhoHq12T82lhozpp9fpMJnJkC1nJrU/A+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.10", + "@inquirer/core": "^12.0.6", + "@inquirer/type": "4.2.1" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/prompts": { + "version": "8.7.3", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-8.7.3.tgz", + "integrity": "sha512-DHK20kO+H5k/XcJQpHQUpVQbMYHHhZchlGWhZVMUV4WIuRuBW5744hipOtriE1vMqPLgnIyXFMHWKkIbTepa4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/checkbox": "^5.2.6", + "@inquirer/confirm": "^6.3.3", + "@inquirer/editor": "^5.3.4", + "@inquirer/expand": "^5.1.6", + "@inquirer/input": "^5.1.7", + "@inquirer/number": "^4.2.4", + "@inquirer/password": "^5.2.3", + "@inquirer/rawlist": "^5.3.6", + "@inquirer/search": "^4.3.4", + "@inquirer/select": "^5.2.6" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/rawlist": { + "version": "5.3.8", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-5.3.8.tgz", + "integrity": "sha512-h+6Y4pPvYvuRbksn1kIBdD9tCYD+E468W5sIcVfLmoLP5IasnY0r33N+r+46ej/P8b2PCREjyKR+MnnfPGdTzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^12.0.6", + "@inquirer/type": "4.2.1" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/search": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-4.3.6.tgz", + "integrity": "sha512-JZj9ttkIvuaFQIc2oxsyqC1ad05WL02ku6W/INVKTCzNaVq1KkhP3AfTRC0m7OWlKZyw+EWwPBFZfYiG9jF6Yw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^12.0.6", + "@inquirer/figures": "^2.0.10", + "@inquirer/type": "4.2.1" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/select": { + "version": "5.2.8", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-5.2.8.tgz", + "integrity": "sha512-BklkkAAOpjdCfuQw5fVGcDuhKxA7T1iBpn6b2JUvDDdkIsO3Ftlg+WnWUCVpkUBFagL2o036OFW+xL4lC9wIPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.10", + "@inquirer/core": "^12.0.6", + "@inquirer/figures": "^2.0.10", + "@inquirer/type": "4.2.1" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/type": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-4.2.1.tgz", + "integrity": "sha512-lDkPsnv5M5CcYB2T1qTc3dobNTPkVr4DU3Ic2Q6SW8/qVRHgvzdRdmVZ6YqG9Ag1aJTeMsLU0s0WPRh3CZWDQQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" }, "peerDependencies": { - "hono": "^4 || ^5.0.0-0" + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, "node_modules/@jridgewell/gen-mapping": { @@ -489,6 +734,17 @@ "@jridgewell/trace-mapping": "^0.3.24" } }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", @@ -517,6 +773,40 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@keyv/bigmap": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@keyv/bigmap/-/bigmap-1.3.1.tgz", + "integrity": "sha512-WbzE9sdmQtKy8vrNPa9BRnwZh5UF4s1KTmSK0KUVLo3eff5BlQNNWDnFOouNpKfPKDnms9xynJjsMYjMaT/aFQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "hashery": "^1.4.0", + "hookified": "^1.15.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "keyv": "^5.6.0" + } + }, + "node_modules/@keyv/serialize": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.1.1.tgz", + "integrity": "sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@lukeed/csprng": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@lukeed/csprng/-/csprng-1.1.0.tgz", + "integrity": "sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/@napi-rs/lzma-linux-x64-gnu": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", @@ -534,10 +824,44 @@ "node": "^22.20 || ^24.12 || >=25" } }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.67.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.67.4.tgz", - "integrity": "sha512-/v/pUAPOcwp+r1Hjjdg0cBV1TE89CzGKrKh2FgkIQw2QBcIMmVPOCSfuOZStCn4Xh/98HXfo/ht6ztcssctPNg==", + "node_modules/@napi-rs/nice": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice/-/nice-1.1.1.tgz", + "integrity": "sha512-xJIPs+bYuc9ASBl+cvGsKbGrJmS6fAKaSZCnT0lhahT5rhA2VVy9/EcIgd2JhtEuFOJNx7UHNn/qiTPTY4nrQw==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "optionalDependencies": { + "@napi-rs/nice-android-arm-eabi": "1.1.1", + "@napi-rs/nice-android-arm64": "1.1.1", + "@napi-rs/nice-darwin-arm64": "1.1.1", + "@napi-rs/nice-darwin-x64": "1.1.1", + "@napi-rs/nice-freebsd-x64": "1.1.1", + "@napi-rs/nice-linux-arm-gnueabihf": "1.1.1", + "@napi-rs/nice-linux-arm64-gnu": "1.1.1", + "@napi-rs/nice-linux-arm64-musl": "1.1.1", + "@napi-rs/nice-linux-ppc64-gnu": "1.1.1", + "@napi-rs/nice-linux-riscv64-gnu": "1.1.1", + "@napi-rs/nice-linux-s390x-gnu": "1.1.1", + "@napi-rs/nice-linux-x64-gnu": "1.1.1", + "@napi-rs/nice-linux-x64-musl": "1.1.1", + "@napi-rs/nice-openharmony-arm64": "1.1.1", + "@napi-rs/nice-win32-arm64-msvc": "1.1.1", + "@napi-rs/nice-win32-ia32-msvc": "1.1.1", + "@napi-rs/nice-win32-x64-msvc": "1.1.1" + } + }, + "node_modules/@napi-rs/nice-android-arm-eabi": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-android-arm-eabi/-/nice-android-arm-eabi-1.1.1.tgz", + "integrity": "sha512-kjirL3N6TnRPv5iuHw36wnucNqXAO46dzK9oPb0wj076R5Xm8PfUVA9nAFB5ZNMmfJQJVKACAPd/Z2KYMppthw==", "cpu": [ "arm" ], @@ -546,12 +870,15 @@ "optional": true, "os": [ "android" - ] + ], + "engines": { + "node": ">= 10" + } }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.67.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.67.4.tgz", - "integrity": "sha512-6tmmB/P52Vt167FXXEw5auiEUgTtZz7aqUgWHqYXToIUE0HFDY6fTdAyeekjjsNk6O/8dtpSKtClxSj1mMneKQ==", + "node_modules/@napi-rs/nice-android-arm64": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-android-arm64/-/nice-android-arm64-1.1.1.tgz", + "integrity": "sha512-blG0i7dXgbInN5urONoUCNf+DUEAavRffrO7fZSeoRMJc5qD+BJeNcpr54msPF6qfDD6kzs9AQJogZvT2KD5nw==", "cpu": [ "arm64" ], @@ -560,26 +887,15 @@ "optional": true, "os": [ "android" - ] + ], + "engines": { + "node": ">= 10" + } }, - "node_modules/@rollup/rollup-android-x64": { - "version": "4.67.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-x64/-/rollup-android-x64-4.67.4.tgz", - "integrity": "sha512-8Z9Q4RmotVmuL4llOvqX814JNiOK1oqoBv+glG5KbpRfUiypqzz2/fxIzK4Pq129l4dT5a8Gszmpc0/XPYoFuQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.67.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.67.4.tgz", - "integrity": "sha512-pnq6hssxTtwuRAl6Kdi/+IDY+b1IhkUetjhUkdLAAJCssjfqtL2Oqy/FXJVJFtAWOu1oRlArIvf0Yk1O92x2HQ==", + "node_modules/@napi-rs/nice-darwin-arm64": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-darwin-arm64/-/nice-darwin-arm64-1.1.1.tgz", + "integrity": "sha512-s/E7w45NaLqTGuOjC2p96pct4jRfo61xb9bU1unM/MJ/RFkKlJyJDx7OJI/O0ll/hrfpqKopuAFDV8yo0hfT7A==", "cpu": [ "arm64" ], @@ -588,12 +904,15 @@ "optional": true, "os": [ "darwin" - ] + ], + "engines": { + "node": ">= 10" + } }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.67.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.67.4.tgz", - "integrity": "sha512-BeIrGZvHMVdJg/N+ohZfFywG/gF41684YhDj2wBvouj7m9ziP1tZDUNwzXRiDY+3WIKngaC4hiUBqVtBPGP5og==", + "node_modules/@napi-rs/nice-darwin-x64": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-darwin-x64/-/nice-darwin-x64-1.1.1.tgz", + "integrity": "sha512-dGoEBnVpsdcC+oHHmW1LRK5eiyzLwdgNQq3BmZIav+9/5WTZwBYX7r5ZkQC07Nxd3KHOCkgbHSh4wPkH1N1LiQ==", "cpu": [ "x64" ], @@ -602,26 +921,15 @@ "optional": true, "os": [ "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.67.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.67.4.tgz", - "integrity": "sha512-fRFX/oqNg/g780DKoS8RIfYcZD59Ru5O/bSeK1N7gQljBFrTxJY/q5EX4eMs5SrJf32VgVBVVYCdfPpzAwPPnQ==", - "cpu": [ - "arm64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] + "engines": { + "node": ">= 10" + } }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.67.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.67.4.tgz", - "integrity": "sha512-bSrBRopG1q+0RuOsW0NtYTtIODlvNA3T3ebx5WnQVdSxWIoXFmj+rZdHUHdSLgFdZ5M6VGmekYE7r4RMGQXjfg==", + "node_modules/@napi-rs/nice-freebsd-x64": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-freebsd-x64/-/nice-freebsd-x64-1.1.1.tgz", + "integrity": "sha512-kHv4kEHAylMYmlNwcQcDtXjklYp4FCf0b05E+0h6nDHsZ+F0bDe04U/tXNOqrx5CmIAth4vwfkjjUmp4c4JktQ==", "cpu": [ "x64" ], @@ -630,26 +938,15 @@ "optional": true, "os": [ "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.67.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.67.4.tgz", - "integrity": "sha512-L8IvdrJVtl65g1ozwDSyjGPG+FmibhCdFUwmqTItJLw0S3+Oj4QzFs7rfgTUiK7rsPjkCLdG1/GApFxoynXFJQ==", - "cpu": [ - "arm" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": ">= 10" + } }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.67.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.67.4.tgz", - "integrity": "sha512-h7PcoQOzC31jshNpQ48J0akB9tsghjkSjSl5AZ6XsVGTxVsln7t7C4SQyw62tNVSaz2SeHgk4VT2ELbL17BSHQ==", + "node_modules/@napi-rs/nice-linux-arm-gnueabihf": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-arm-gnueabihf/-/nice-linux-arm-gnueabihf-1.1.1.tgz", + "integrity": "sha512-E1t7K0efyKXZDoZg1LzCOLxgolxV58HCkaEkEvIYQx12ht2pa8hoBo+4OB3qh7e+QiBlp1SRf+voWUZFxyhyqg==", "cpu": [ "arm" ], @@ -658,180 +955,155 @@ "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": ">= 10" + } }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.67.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.67.4.tgz", - "integrity": "sha512-KSPuOBNHoOl7AsVjeY1KZci8+0up0U4TFAHU5IzBYN8P+Z6cB9DZq28+JQycZW7f6WLLa/tSzhV7Rdy95bVi+g==", + "node_modules/@napi-rs/nice-linux-arm64-gnu": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-arm64-gnu/-/nice-linux-arm64-gnu-1.1.1.tgz", + "integrity": "sha512-CIKLA12DTIZlmTaaKhQP88R3Xao+gyJxNWEn04wZwC2wmRapNnxCUZkVwggInMJvtVElA+D4ZzOU5sX4jV+SmQ==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": ">= 10" + } }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.67.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.67.4.tgz", - "integrity": "sha512-0E/mKmVk1PlQeQYygIwQm0jlwjANTzGSiNBMPrFx+rdFGFj/2Axh4p6XwcrystQIqXZVudTaGsdbDpzYgJ4wSA==", + "node_modules/@napi-rs/nice-linux-arm64-musl": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-arm64-musl/-/nice-linux-arm64-musl-1.1.1.tgz", + "integrity": "sha512-+2Rzdb3nTIYZ0YJF43qf2twhqOCkiSrHx2Pg6DJaCPYhhaxbLcdlV8hCRMHghQ+EtZQWGNcS2xF4KxBhSGeutg==", "cpu": [ "arm64" ], "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.67.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.67.4.tgz", - "integrity": "sha512-Fvw7dOSc2Sa9UOipRZVI0zOgdLCzsjVyD/6JuHXzUHTXFu7mMgmeX/0/bCVBIut1dz+nWz3pV4WpS/Qg0XVnxw==", - "cpu": [ - "loong64" + "libc": [ + "musl" ], - "dev": true, "license": "MIT", "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.67.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.67.4.tgz", - "integrity": "sha512-jzxbVHaE7V3F+P0wEK7UydQtvv7Twfdtx58jI5/3VRx1jJxyrn+qFwfXmqjVFxPhBl+zjaRs+ZJMFJw7vdHaQA==", - "cpu": [ - "loong64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": ">= 10" + } }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.67.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.67.4.tgz", - "integrity": "sha512-fhtrzp9p4GjEJVaigaap9g6X9nP6xgJtKiLLXSDgpE18HXufA5aqfltGn+KAqMtVZReJZfOrXm//y5YSA9BWbA==", + "node_modules/@napi-rs/nice-linux-ppc64-gnu": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-ppc64-gnu/-/nice-linux-ppc64-gnu-1.1.1.tgz", + "integrity": "sha512-4FS8oc0GeHpwvv4tKciKkw3Y4jKsL7FRhaOeiPei0X9T4Jd619wHNe4xCLmN2EMgZoeGg+Q7GY7BsvwKpL22Tg==", "cpu": [ "ppc64" ], "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.67.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.67.4.tgz", - "integrity": "sha512-uynaF7IUHwAckV0S+yC1f5esy9gB+aqCBsPH5BOck32TYdCuAn2lVG5tvUcFnEsrviGEmRZxE1NHSOieUPYJEg==", - "cpu": [ - "ppc64" + "libc": [ + "glibc" ], - "dev": true, "license": "MIT", "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.67.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.67.4.tgz", - "integrity": "sha512-uaqRFdfb8eIa2bVHN/FE3ndCvR0zV2xv0AwjCfxrqSu6l6fp/54VhjfhslKlf2RIik9L1jwlt4E56PeVX6tQtQ==", - "cpu": [ - "riscv64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": ">= 10" + } }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.67.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.67.4.tgz", - "integrity": "sha512-JZRow7kmxUDEASnxHuVG5itTm+nTXi9BQUABcVUnUEngC+OPwMrpx1uJ9hIA/v+IcAEBL52ktCSPXk5OJn9Jqg==", + "node_modules/@napi-rs/nice-linux-riscv64-gnu": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-riscv64-gnu/-/nice-linux-riscv64-gnu-1.1.1.tgz", + "integrity": "sha512-HU0nw9uD4FO/oGCCk409tCi5IzIZpH2agE6nN4fqpwVlCn5BOq0MS1dXGjXaG17JaAvrlpV5ZeyZwSon10XOXw==", "cpu": [ "riscv64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": ">= 10" + } }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.67.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.67.4.tgz", - "integrity": "sha512-rdrh4e+Lq9fCNKoDPaCqeHXbQCe7yl77Ukl45tlSClwYdZ1gpFGZz1XLJdYtY++FRlfL5aHvb6/0n3GDW/Nxsw==", + "node_modules/@napi-rs/nice-linux-s390x-gnu": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-s390x-gnu/-/nice-linux-s390x-gnu-1.1.1.tgz", + "integrity": "sha512-2YqKJWWl24EwrX0DzCQgPLKQBxYDdBxOHot1KWEq7aY2uYeX+Uvtv4I8xFVVygJDgf6/92h9N3Y43WPx8+PAgQ==", "cpu": [ "s390x" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": ">= 10" + } }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.67.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.67.4.tgz", - "integrity": "sha512-wL7MS4r0fYcxluF3ne2plofHZiB/LsqNCnyaUlyiKpAJLb6agPSFz7DEFCfkonWAZXMrLwCmC08b2ZEnEdJkog==", + "node_modules/@napi-rs/nice-linux-x64-gnu": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-x64-gnu/-/nice-linux-x64-gnu-1.1.1.tgz", + "integrity": "sha512-/gaNz3R92t+dcrfCw/96pDopcmec7oCcAQ3l/M+Zxr82KT4DljD37CpgrnXV+pJC263JkW572pdbP3hP+KjcIg==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": ">= 10" + } }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.67.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.67.4.tgz", - "integrity": "sha512-4H0XHfd2VufGjX7NjUyeue78sD0bRIoKSGjGqO+F5nSvL6jN48NAU7esdmtFEYxZXwObLtt3MJ/kHS0jASMW3g==", + "node_modules/@napi-rs/nice-linux-x64-musl": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-x64-musl/-/nice-linux-x64-musl-1.1.1.tgz", + "integrity": "sha512-xScCGnyj/oppsNPMnevsBe3pvNaoK7FGvMjT35riz9YdhB2WtTG47ZlbxtOLpjeO9SqqQ2J2igCmz6IJOD5JYw==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.67.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.67.4.tgz", - "integrity": "sha512-osRHQbE55pZpCLetpMsFmhkapyZnAUH73qRKMh+npi3boUom6u6hdaTTPSxlInXxwJHsn9r2uDhdz0V7Yoxx2A==", - "cpu": [ - "x64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ] + "engines": { + "node": ">= 10" + } }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.67.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.67.4.tgz", - "integrity": "sha512-95+exYEyOwrZePsE0czi4q7X5lxKPa21+lmyAZlwbYENITQ0Unk4HAQAFp/p+KhXBHBx9mUABXRYjOC5A1wY3Q==", + "node_modules/@napi-rs/nice-openharmony-arm64": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-openharmony-arm64/-/nice-openharmony-arm64-1.1.1.tgz", + "integrity": "sha512-6uJPRVwVCLDeoOaNyeiW0gp2kFIM4r7PL2MczdZQHkFi9gVlgm+Vn+V6nTWRcu856mJ2WjYJiumEajfSm7arPQ==", "cpu": [ "arm64" ], @@ -840,12 +1112,15 @@ "optional": true, "os": [ "openharmony" - ] + ], + "engines": { + "node": ">= 10" + } }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.67.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.67.4.tgz", - "integrity": "sha512-FOXOcKhNQpO4Po+Ynan6IbtM67SrXZbgpRJWVYwz53CT5veI7sVzDwVBmWS65N5Imv+wK3SVhIPAl+C5DXXNCg==", + "node_modules/@napi-rs/nice-win32-arm64-msvc": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-win32-arm64-msvc/-/nice-win32-arm64-msvc-1.1.1.tgz", + "integrity": "sha512-uoTb4eAvM5B2aj/z8j+Nv8OttPf2m+HVx3UjA5jcFxASvNhQriyCQF1OB1lHL43ZhW+VwZlgvjmP5qF3+59atA==", "cpu": [ "arm64" ], @@ -854,12 +1129,15 @@ "optional": true, "os": [ "win32" - ] + ], + "engines": { + "node": ">= 10" + } }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.67.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.67.4.tgz", - "integrity": "sha512-cklKh0gJOkAZQcWziQa3hrjxZKN+PCHZ7SPTyGJDAiBVr05ZXlEjGFCG7zFJMaVibxsmzAe1FimN4u/HBns9+Q==", + "node_modules/@napi-rs/nice-win32-ia32-msvc": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-win32-ia32-msvc/-/nice-win32-ia32-msvc-1.1.1.tgz", + "integrity": "sha512-CNQqlQT9MwuCsg1Vd/oKXiuH+TcsSPJmlAFc5frFyX/KkOh0UpBLEj7aoY656d5UKZQMQFP7vJNa1DNUNORvug==", "cpu": [ "ia32" ], @@ -868,26 +1146,15 @@ "optional": true, "os": [ "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.67.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.67.4.tgz", - "integrity": "sha512-AegwxJPc9bvUp8fQtlsUHkFMOMkGylWU7glyL+YD4vwG+QaYoGXYLSsQoeTD7kvoLZrVlMWNVhTg1dedY5C1VQ==", - "cpu": [ - "x64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] + "engines": { + "node": ">= 10" + } }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.67.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.67.4.tgz", - "integrity": "sha512-RJZnrwTWj7Iq2ooBwoGbDdC65tDwYawneGcAfFMJqVUrRZMcskka6nTkBpXR/47G7kzn3EW2qupExnaBy11vwg==", + "node_modules/@napi-rs/nice-win32-x64-msvc": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-win32-x64-msvc/-/nice-win32-x64-msvc-1.1.1.tgz", + "integrity": "sha512-vB+4G/jBQCAh0jelMTY3+kgFy00Hlx2f2/1zjMoH821IbplbWZOkLiTYXQkygNTzQJTq5cvwBDgn2ppHD+bglQ==", "cpu": [ "x64" ], @@ -896,131 +1163,4902 @@ "optional": true, "os": [ "win32" - ] + ], + "engines": { + "node": ">= 10" + } }, - "node_modules/@types/better-sqlite3": { - "version": "7.6.13", - "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz", - "integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==", + "node_modules/@nestjs/cli": { + "version": "12.0.11", + "resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-12.0.11.tgz", + "integrity": "sha512-ExNgbKfhwMNXCV6FbQQnQOcxuubkDPcpYXUh+jkFn5Xm+WkdP+wjYS0KqyZgFSfpY24mST6y50UOEHVsvQME1A==", "dev": true, "license": "MIT", "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/estree": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.10.tgz", + "@angular-devkit/core": "22.2.3", + "@angular-devkit/schematics": "22.2.3", + "@angular-devkit/schematics-cli": "22.2.3", + "@inquirer/prompts": "8.7.3", + "@nestjs/schematics": "^12.0.0", + "ansis": "4.4.0", + "chokidar": "5.0.0", + "cli-table3": "0.6.5", + "commander": "15.0.0", + "jsonc-parser": "3.3.1", + "minimatch": "10.2.6", + "node-emoji": "2.2.0", + "ora": "9.4.1", + "tsconfig-paths": "4.2.0", + "typescript": "~6.0.2" + }, + "bin": { + "nest": "bin/nest.js" + }, + "engines": { + "node": ">= 20.11" + }, + "peerDependencies": { + "@rspack/core": "^1.7.7 || ^2.1.10", + "@swc/cli": "^0.8.0", + "@swc/core": "^1.15.18", + "fork-ts-checker-webpack-plugin": "^9.1.0", + "ts-loader": "^9.5.4", + "tsconfig-paths-webpack-plugin": "^4.2.0", + "webpack": "^5.105.4", + "webpack-node-externals": "^3.0.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "@swc/cli": { + "optional": true + }, + "@swc/core": { + "optional": true + }, + "fork-ts-checker-webpack-plugin": { + "optional": true + }, + "ts-loader": { + "optional": true + }, + "tsconfig-paths-webpack-plugin": { + "optional": true + }, + "webpack": { + "optional": true + }, + "webpack-node-externals": { + "optional": true + } + } + }, + "node_modules/@nestjs/cli/node_modules/@nestjs/schematics": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-12.0.0.tgz", + "integrity": "sha512-ilsxam7i+SWd2psr12faLVSk86FYKnxRUGO6jAGNi1P9+O0kRaYVOScyrKeIj97W9T9UJoJF38fg+7KgjY/nOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "22.1.5", + "@angular-devkit/schematics": "22.1.5", + "comment-json": "5.0.0", + "jsonc-parser": "3.3.1", + "pluralize": "8.0.0" + }, + "engines": { + "node": ">=22.12.0" + }, + "peerDependencies": { + "prettier": "^3.0.0", + "typescript": ">=6.0.0" + }, + "peerDependenciesMeta": { + "prettier": { + "optional": true + } + } + }, + "node_modules/@nestjs/cli/node_modules/@nestjs/schematics/node_modules/@angular-devkit/core": { + "version": "22.1.5", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-22.1.5.tgz", + "integrity": "sha512-HiY6d5dkIdJs5grP9OHvgkf14QOcDIo+hbuT7YKuLItQ++ZxCwUabJMLCCJ5R0KrstE5NqED0dNcyk5WD01t5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "8.20.0", + "ajv-formats": "3.0.1", + "jsonc-parser": "3.3.1", + "picomatch": "4.0.5", + "rxjs": "7.8.2", + "source-map": "0.7.6" + }, + "engines": { + "node": "^22.22.3 || ^24.15.0 || >=26.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^5.0.0" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@nestjs/cli/node_modules/@nestjs/schematics/node_modules/@angular-devkit/schematics": { + "version": "22.1.5", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-22.1.5.tgz", + "integrity": "sha512-HTmo9y8wjXKtJGVTYomTjZsjWzhirpu1pPRAzsVob3eiYOuxv1qDRAgWiHXNGp5CpnbHunZweXY/WffNGOFRyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "22.1.5", + "jsonc-parser": "3.3.1", + "magic-string": "1.0.0", + "ora": "9.4.1", + "rxjs": "7.8.2" + }, + "engines": { + "node": "^22.22.3 || ^24.15.0 || >=26.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@nestjs/cli/node_modules/magic-string": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.0.0.tgz", + "integrity": "sha512-CGvjzMN08iv6w1mm4/x3Gh1hLb4VnyRUA15FFpl6CsCIGGoe36k7kY5KNz9QDbSBN5I/fWHM6ZlIkUTa5xdUEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/@nestjs/cli/node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@nestjs/cli/node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "node_modules/@nestjs/common": { + "version": "12.3.2", + "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-12.3.2.tgz", + "integrity": "sha512-XPNu75rXiDBuQ/EECiA0urFJ7bAycA1/xTrKnKct5vEF7A5FE0LyOem/EhRZI0Cq1xAGPDuTB89sFMqeSPWjzQ==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "1.1.0", + "file-type": "22.2.0", + "iterare": "1.2.1", + "load-esm": "1.0.3", + "tslib": "2.8.1", + "uid": "2.0.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "class-transformer": ">=0.4.1", + "class-validator": ">=0.13.2", + "reflect-metadata": "^0.1.12 || ^0.2.0", + "rxjs": "^7.1.0" + }, + "peerDependenciesMeta": { + "class-transformer": { + "optional": true + }, + "class-validator": { + "optional": true + } + } + }, + "node_modules/@nestjs/core": { + "version": "12.3.2", + "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-12.3.2.tgz", + "integrity": "sha512-lCDN5iC4BoG9k5Sk/tG2EF4VAtoZABFjN4e0nk9xKK3ytD4eSMc9ma1Y092t/navenlf3roTtCDUZ/RIHzYjCw==", + "license": "MIT", + "dependencies": { + "fast-safe-stringify": "2.1.1", + "iterare": "1.2.1", + "path-to-regexp": "8.4.2", + "tslib": "2.8.1", + "uid": "2.0.2" + }, + "engines": { + "node": ">= 20" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "@nestjs/common": "^12.0.0", + "@nestjs/microservices": "^12.0.0", + "@nestjs/platform-express": "^12.0.0", + "@nestjs/websockets": "^12.0.0", + "reflect-metadata": "^0.1.12 || ^0.2.0", + "rxjs": "^7.1.0" + }, + "peerDependenciesMeta": { + "@nestjs/microservices": { + "optional": true + }, + "@nestjs/platform-express": { + "optional": true + }, + "@nestjs/websockets": { + "optional": true + } + } + }, + "node_modules/@nestjs/platform-express": { + "version": "12.3.2", + "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-12.3.2.tgz", + "integrity": "sha512-wbe1zd6xWLW8qI6t3RP3SqqQnp32pVAfXKqttcp/geL5ys6xw6c/HkASXLNvwlf7IzMLxnpqANtHHwcF4waqRA==", + "license": "MIT", + "dependencies": { + "cors": "2.8.6", + "express": "5.3.0", + "multer": "2.5.0", + "path-to-regexp": "8.4.2", + "tslib": "2.8.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "@nestjs/common": "^12.0.0", + "@nestjs/core": "^12.0.0" + } + }, + "node_modules/@nestjs/testing": { + "version": "12.3.2", + "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-12.3.2.tgz", + "integrity": "sha512-l1m+uHTp4hJ1K+Gw8ENrLSj2TxZlKVre7aoluUdxMHCodK2kZnpjvD8H8Oh460hj/Z6PDJ/m9r6t8NIK6OyDxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "@nestjs/common": "^12.0.0", + "@nestjs/core": "^12.0.0", + "@nestjs/microservices": "^12.0.0", + "@nestjs/platform-express": "^12.0.0" + }, + "peerDependenciesMeta": { + "@nestjs/microservices": { + "optional": true + }, + "@nestjs/platform-express": { + "optional": true + } + } + }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@oxfmt/binding-android-arm-eabi": { + "version": "0.75.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-android-arm-eabi/-/binding-android-arm-eabi-0.75.0.tgz", + "integrity": "sha512-zHJ7LcTN46/5mtoLxsUu14pdF9ixFwDURtmxZ0CeNwqom5qB5NRVqU6o2GRZ7QPdNBW8G6j9UTJzQ+EDevMH0w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-android-arm64": { + "version": "0.75.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-android-arm64/-/binding-android-arm64-0.75.0.tgz", + "integrity": "sha512-IdxsKZJvHZUvI7q6kLmzXdEFpFiBEDQw/WJtSp1QlzNXBw1JlLN3tY50WwWd4doPnq86XW4P+7BsQwu6meCW+w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-darwin-arm64": { + "version": "0.75.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-darwin-arm64/-/binding-darwin-arm64-0.75.0.tgz", + "integrity": "sha512-vyiqZilO5jal/nXZ0143NyKcP6TkzjaQdVEWU/kwgyWqugAgIRgAbrnO3MOF+e6LkQu3CrtrkPoc3mrsZZY6cA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-darwin-x64": { + "version": "0.75.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-darwin-x64/-/binding-darwin-x64-0.75.0.tgz", + "integrity": "sha512-oua7eVqVe769mDam6ORcavygxTzG3ovo5QRQ1kJCPaEbnWXPYhtAiYDj8pajVmFjO7j8Elf3gInnWpTX0LLYtg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-freebsd-x64": { + "version": "0.75.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-freebsd-x64/-/binding-freebsd-x64-0.75.0.tgz", + "integrity": "sha512-NWkbezhg7mx2Ep94g/TysR3c1bsISsU4lQ6t5xI4841PAk0Kqcn1+UEe+BoFH2/UK2YegPXq+OF+0bUcSvUalA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-arm-gnueabihf": { + "version": "0.75.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.75.0.tgz", + "integrity": "sha512-CAHX87kJplTq2E0OWP7opG8gEVlHc7CeLEi4nMJOyx3psuqBKf84HZ3UIkX2guWEJ5UljU2zJF0o46XylywVVA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-arm-musleabihf": { + "version": "0.75.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.75.0.tgz", + "integrity": "sha512-jmxYOiQzx2fp+OwQb7717K1zI8AMKEykw04sfr95/mU8SIL/oKgQq8nXH42sEznEquBL2RU7nLY36eBnYFT+vg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-arm64-gnu": { + "version": "0.75.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.75.0.tgz", + "integrity": "sha512-eimaiAhx08dbOg12Fj3PYhymXY8vRbaiJB505vDXMy6ohne3Y1qTS0e5KEwFMq3LmDjEO+16BX38c/oXFI/SAQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-arm64-musl": { + "version": "0.75.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.75.0.tgz", + "integrity": "sha512-bjSONIG1ba2xugr9oHQau0E6oPFYOMjPaa5hK/wpVC/Q1pcgf2BZMKwbuA8/cWzhWX5QV/UdPzD+rw1kQHpikQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-ppc64-gnu": { + "version": "0.75.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.75.0.tgz", + "integrity": "sha512-wDksWQXC/KZucISyqlhBtvpW+pyHh8z0Cjm6Qxiv22XlllM2Bx5gJGDnPYjV+H0lL+o1iqLlEli8jmBuRCLcjw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-riscv64-gnu": { + "version": "0.75.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.75.0.tgz", + "integrity": "sha512-5JkNwViqaVuk4I+9+zgVrEYiHOAPXFxgDgP6iDnpdOBjm7XAHAOjSvolHZZAokWPx7CGOw8tKteXmn4WPIInaQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-riscv64-musl": { + "version": "0.75.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.75.0.tgz", + "integrity": "sha512-1EVqRIE0eZLB08j3ZCVN47Ors7LCXGmOI+3xJYgyoFUKUerD3Glxb+Q1Bp3lK8NeT5R3ZdS9uhX/wobNBYZ6jw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-s390x-gnu": { + "version": "0.75.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.75.0.tgz", + "integrity": "sha512-UJWRVNT6Yuw4ssLv2BwaBUWU1GWtByE+EnoHu8I+P1t8Kn+WozYU5WYOqb5aj8gbXJJ8gGZt+w40Hi/7L6iVLw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-x64-gnu": { + "version": "0.75.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.75.0.tgz", + "integrity": "sha512-FHdvHhyz8ChBctBUvVGti49kSEwANd1PZQwlXUEoF9Ed4plxXxk/wWc0mfZmrze2lRgmME7ytDmVIOixYauQhg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-x64-musl": { + "version": "0.75.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-x64-musl/-/binding-linux-x64-musl-0.75.0.tgz", + "integrity": "sha512-6MjtsF96s9mSBV0GEwUe4pOFPP2Mz7ahcHPAit0DSKL5fM6oMM7RG1zvYxwB7+SBkIxsWBRhhxuqmy7pibNwsA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-openharmony-arm64": { + "version": "0.75.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-openharmony-arm64/-/binding-openharmony-arm64-0.75.0.tgz", + "integrity": "sha512-1D6T6aL1Pj+U5GLxqwV1fKjUJ5+FFm+bn7UrqNDMveZmWs0jo/3vc+dzzuV/jF/FdVo12gowODk3SnAO2YOwLg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-win32-arm64-msvc": { + "version": "0.75.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.75.0.tgz", + "integrity": "sha512-RX2eUmy7J/nM8+tnM3nzR8t1eqkwj1q8Kgo7beA+4DVgKYYHHyrEa3qEmthxw7Z+HqpCCEOWuoqa+jelo9/w2Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-win32-ia32-msvc": { + "version": "0.75.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.75.0.tgz", + "integrity": "sha512-UpKta0iSZmPLII0dYXYdbzdBTsGREUbSHLqgLdtKcqBvs40qjmUz+cgUcaU1smPx13DfbEG4XqnaSn5MoFN0og==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-win32-x64-msvc": { + "version": "0.75.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.75.0.tgz", + "integrity": "sha512-pjiLiCk0QMCWxs0n5XiNU85YLAKA+wJxPjxlDVygmr4bP4glcfTU2nOpso5nR4Rdi2Pxv5YwakD4UE+eh7M0BQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-android-arm-eabi": { + "version": "1.90.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.90.0.tgz", + "integrity": "sha512-sgfalb1HuxYjiAa4wESZ626z1FGusqNcXYHyB5oIOEprS2sbPg0UYEfo83uF1GNG70DIfZxHkXuE6KFrlSdn8Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-android-arm64": { + "version": "1.90.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm64/-/binding-android-arm64-1.90.0.tgz", + "integrity": "sha512-RcKhz1ETMq+fazE9TKS2+5NIRGwigm/nW8egqF0LI+A8Yw2iW5r0E/C3XAPRiwIm/vcPiutIRIa6IQCIKhZsTQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-darwin-arm64": { + "version": "1.90.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-arm64/-/binding-darwin-arm64-1.90.0.tgz", + "integrity": "sha512-PSrxMvP6mx+p1karZcUxLdy9lpaP+JWQPti4al9FgTaPhxE+pT/BjhGExloVVSWgZgIfOlLOjVyyq8BXlodhLQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-darwin-x64": { + "version": "1.90.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-x64/-/binding-darwin-x64-1.90.0.tgz", + "integrity": "sha512-OLbU9qsrV2Q8QxT2OEzOPdO8zCCpyI8qB+hDI8yr6RsYELtkf0FAZhJdfjM/RnLHKzt44HWwAPnk92vBtKofvw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-freebsd-x64": { + "version": "1.90.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-freebsd-x64/-/binding-freebsd-x64-1.90.0.tgz", + "integrity": "sha512-fBA4pWyVDARmNucQqpyFuPC49xYf1r+sX7dxPL5fNPQrSMLum39Sn56tZ9JPleWC7FzLPwSQ7plYYfirYBHF8A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm-gnueabihf": { + "version": "1.90.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.90.0.tgz", + "integrity": "sha512-zhxH78eP2nR6/pGdhBc7MIfjhhcgATeu8fkmkFJbuzfk6Cj0FzBxyEhL85yDqrCybjx+G2YDzZGsxIO9kvO4Iw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm-musleabihf": { + "version": "1.90.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-1.90.0.tgz", + "integrity": "sha512-MvYmpHyJqUJjEvkZbJHdwfdL45cGn8EVBkYmoXFiaYFW9suoD3dbJl5mBdpy0DVgdjlyddmldbRtk3cCgETadQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm64-gnu": { + "version": "1.90.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.90.0.tgz", + "integrity": "sha512-L13Sy+Kg0nZhwTsToniqjUFPA2TuM6962qCZ5Wp5KeB517Tw7ApdLTTtlb+bF86fcZZa2EtvCVvAJxk4Dya/MQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm64-musl": { + "version": "1.90.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.90.0.tgz", + "integrity": "sha512-wFDdZWGpzIPFdJFjSLWnKB0TGzFriTKUIrl04iMqMyM9BBIJTuerfCHYa0WpDSjr5x3lcW/CHLyacSt886WpGg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-ppc64-gnu": { + "version": "1.90.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.90.0.tgz", + "integrity": "sha512-FRywfn+MoKn8xpBSzgNwNVliSKRI2QXkxi/iF7o6FEQs8Bh8z2RpVTaFAcO3pchXjvB2eGNFIoLLQABIQ+u7VA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-riscv64-gnu": { + "version": "1.90.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-1.90.0.tgz", + "integrity": "sha512-IS2PaUgpc/qeLADTyhUNmdx3x4ijs9pjJdPKBYHaT+mAE7k3FsjnHPTjhpsak1b++h6CIPhv6WX9Z+y7Pw0voQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-riscv64-musl": { + "version": "1.90.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-1.90.0.tgz", + "integrity": "sha512-s7JFKRkGS8O7i2ZVODfuO++NC7sH6HDqpMn8lYxw91w0SXJOxnAVjQ8utvmDwLMMT0jaSkuOKuUYJDGDtu4sHQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-s390x-gnu": { + "version": "1.90.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.90.0.tgz", + "integrity": "sha512-b0GxrOQr5VONCkd7KLSqmP6b6nqZuTpuS8QPQHzltmqdXP5bY0Jl0CZ0QmlCTrS3ExaLYbioFYOARzNbYvcApQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-x64-gnu": { + "version": "1.90.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.90.0.tgz", + "integrity": "sha512-iaJeShJtgUp8rZbt9YUdTAGOpmX5KogCLba17Jp1tWdJ3F4M3D2g2TKUyI1FMTHxKH9NNHuQcuC2kZRg6IV7yQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-x64-musl": { + "version": "1.90.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-musl/-/binding-linux-x64-musl-1.90.0.tgz", + "integrity": "sha512-/aZoXnL5WBLCXuzfHqvEFXcm4iwYmr/cYByHDTXzLFzJdE5DC7bm6phpWqHv6BeqpVgY9IpBAxGDv2WEkl5JCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-openharmony-arm64": { + "version": "1.90.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-openharmony-arm64/-/binding-openharmony-arm64-1.90.0.tgz", + "integrity": "sha512-SMhk7ozwoxYDKfYqGZCQEfULOKDneF9uhaoIf2neoDLWvNtjY9yVeL7YHmE5L9WdKvMpWNwwZxhenxBu6AgfOg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-win32-arm64-msvc": { + "version": "1.90.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.90.0.tgz", + "integrity": "sha512-KBvR3A9fO7fCi2vCP8ifWvlia7tGEQV9NaYh+IGwa/7Groy48Mt/UaYqMK9/zOtAWl20BUCf6RPqzJ32fRw33Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-win32-ia32-msvc": { + "version": "1.90.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.90.0.tgz", + "integrity": "sha512-+x42rDHQGbTCuT3EzpWAzhP3NoVPE/cJsG4gnrakOSg0olV+GCdMpPU23Nvrl3zZMnXxFZdTtDv4D3SuaA5ZeQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-win32-x64-msvc": { + "version": "1.90.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.90.0.tgz", + "integrity": "sha512-30JiaFEBUB9peEOcvVM8ggo88SoN/IdSr9DW505JdBji/R6dM66gKEiG65hw6qcaF2f4mO0cEJ+RwRZprmun9g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@paralleldrive/cuid2": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.3.1.tgz", + "integrity": "sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "^1.1.5" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/pluginutils": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.5.1.tgz", + "integrity": "sha512-P9ZvnfouRu4MSC+ilERaw6FmZs8zqKhpHxEs4aGDGWQAMzQGXnhvCSvoNwokfNvdt7t3Y5Y4p6gStQgezeIqug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils/node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.67.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.67.4.tgz", + "integrity": "sha512-/v/pUAPOcwp+r1Hjjdg0cBV1TE89CzGKrKh2FgkIQw2QBcIMmVPOCSfuOZStCn4Xh/98HXfo/ht6ztcssctPNg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.67.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.67.4.tgz", + "integrity": "sha512-6tmmB/P52Vt167FXXEw5auiEUgTtZz7aqUgWHqYXToIUE0HFDY6fTdAyeekjjsNk6O/8dtpSKtClxSj1mMneKQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-x64": { + "version": "4.67.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-x64/-/rollup-android-x64-4.67.4.tgz", + "integrity": "sha512-8Z9Q4RmotVmuL4llOvqX814JNiOK1oqoBv+glG5KbpRfUiypqzz2/fxIzK4Pq129l4dT5a8Gszmpc0/XPYoFuQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.67.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.67.4.tgz", + "integrity": "sha512-pnq6hssxTtwuRAl6Kdi/+IDY+b1IhkUetjhUkdLAAJCssjfqtL2Oqy/FXJVJFtAWOu1oRlArIvf0Yk1O92x2HQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.67.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.67.4.tgz", + "integrity": "sha512-BeIrGZvHMVdJg/N+ohZfFywG/gF41684YhDj2wBvouj7m9ziP1tZDUNwzXRiDY+3WIKngaC4hiUBqVtBPGP5og==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.67.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.67.4.tgz", + "integrity": "sha512-fRFX/oqNg/g780DKoS8RIfYcZD59Ru5O/bSeK1N7gQljBFrTxJY/q5EX4eMs5SrJf32VgVBVVYCdfPpzAwPPnQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.67.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.67.4.tgz", + "integrity": "sha512-bSrBRopG1q+0RuOsW0NtYTtIODlvNA3T3ebx5WnQVdSxWIoXFmj+rZdHUHdSLgFdZ5M6VGmekYE7r4RMGQXjfg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.67.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.67.4.tgz", + "integrity": "sha512-L8IvdrJVtl65g1ozwDSyjGPG+FmibhCdFUwmqTItJLw0S3+Oj4QzFs7rfgTUiK7rsPjkCLdG1/GApFxoynXFJQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.67.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.67.4.tgz", + "integrity": "sha512-h7PcoQOzC31jshNpQ48J0akB9tsghjkSjSl5AZ6XsVGTxVsln7t7C4SQyw62tNVSaz2SeHgk4VT2ELbL17BSHQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.67.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.67.4.tgz", + "integrity": "sha512-KSPuOBNHoOl7AsVjeY1KZci8+0up0U4TFAHU5IzBYN8P+Z6cB9DZq28+JQycZW7f6WLLa/tSzhV7Rdy95bVi+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.67.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.67.4.tgz", + "integrity": "sha512-0E/mKmVk1PlQeQYygIwQm0jlwjANTzGSiNBMPrFx+rdFGFj/2Axh4p6XwcrystQIqXZVudTaGsdbDpzYgJ4wSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.67.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.67.4.tgz", + "integrity": "sha512-Fvw7dOSc2Sa9UOipRZVI0zOgdLCzsjVyD/6JuHXzUHTXFu7mMgmeX/0/bCVBIut1dz+nWz3pV4WpS/Qg0XVnxw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.67.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.67.4.tgz", + "integrity": "sha512-jzxbVHaE7V3F+P0wEK7UydQtvv7Twfdtx58jI5/3VRx1jJxyrn+qFwfXmqjVFxPhBl+zjaRs+ZJMFJw7vdHaQA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.67.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.67.4.tgz", + "integrity": "sha512-fhtrzp9p4GjEJVaigaap9g6X9nP6xgJtKiLLXSDgpE18HXufA5aqfltGn+KAqMtVZReJZfOrXm//y5YSA9BWbA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.67.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.67.4.tgz", + "integrity": "sha512-uynaF7IUHwAckV0S+yC1f5esy9gB+aqCBsPH5BOck32TYdCuAn2lVG5tvUcFnEsrviGEmRZxE1NHSOieUPYJEg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.67.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.67.4.tgz", + "integrity": "sha512-uaqRFdfb8eIa2bVHN/FE3ndCvR0zV2xv0AwjCfxrqSu6l6fp/54VhjfhslKlf2RIik9L1jwlt4E56PeVX6tQtQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.67.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.67.4.tgz", + "integrity": "sha512-JZRow7kmxUDEASnxHuVG5itTm+nTXi9BQUABcVUnUEngC+OPwMrpx1uJ9hIA/v+IcAEBL52ktCSPXk5OJn9Jqg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.67.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.67.4.tgz", + "integrity": "sha512-rdrh4e+Lq9fCNKoDPaCqeHXbQCe7yl77Ukl45tlSClwYdZ1gpFGZz1XLJdYtY++FRlfL5aHvb6/0n3GDW/Nxsw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.67.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.67.4.tgz", + "integrity": "sha512-wL7MS4r0fYcxluF3ne2plofHZiB/LsqNCnyaUlyiKpAJLb6agPSFz7DEFCfkonWAZXMrLwCmC08b2ZEnEdJkog==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.67.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.67.4.tgz", + "integrity": "sha512-4H0XHfd2VufGjX7NjUyeue78sD0bRIoKSGjGqO+F5nSvL6jN48NAU7esdmtFEYxZXwObLtt3MJ/kHS0jASMW3g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.67.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.67.4.tgz", + "integrity": "sha512-osRHQbE55pZpCLetpMsFmhkapyZnAUH73qRKMh+npi3boUom6u6hdaTTPSxlInXxwJHsn9r2uDhdz0V7Yoxx2A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.67.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.67.4.tgz", + "integrity": "sha512-95+exYEyOwrZePsE0czi4q7X5lxKPa21+lmyAZlwbYENITQ0Unk4HAQAFp/p+KhXBHBx9mUABXRYjOC5A1wY3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.67.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.67.4.tgz", + "integrity": "sha512-FOXOcKhNQpO4Po+Ynan6IbtM67SrXZbgpRJWVYwz53CT5veI7sVzDwVBmWS65N5Imv+wK3SVhIPAl+C5DXXNCg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.67.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.67.4.tgz", + "integrity": "sha512-cklKh0gJOkAZQcWziQa3hrjxZKN+PCHZ7SPTyGJDAiBVr05ZXlEjGFCG7zFJMaVibxsmzAe1FimN4u/HBns9+Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.67.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.67.4.tgz", + "integrity": "sha512-AegwxJPc9bvUp8fQtlsUHkFMOMkGylWU7glyL+YD4vwG+QaYoGXYLSsQoeTD7kvoLZrVlMWNVhTg1dedY5C1VQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.67.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.67.4.tgz", + "integrity": "sha512-RJZnrwTWj7Iq2ooBwoGbDdC65tDwYawneGcAfFMJqVUrRZMcskka6nTkBpXR/47G7kzn3EW2qupExnaBy11vwg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@sec-ant/readable-stream": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", + "integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sindresorhus/is": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, + "node_modules/@stylistic/eslint-plugin": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin/-/eslint-plugin-4.4.1.tgz", + "integrity": "sha512-CEigAk7eOLyHvdgmpZsKFwtiqS2wFwI1fn4j09IU9GmD4euFM4jEBAViWeCqaNLlbX2k2+A/Fq9cje4HQBXuJQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/utils": "^8.32.1", + "eslint-visitor-keys": "^4.2.0", + "espree": "^10.3.0", + "estraverse": "^5.3.0", + "picomatch": "^4.0.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "peerDependencies": { + "eslint": ">=9.0.0" + } + }, + "node_modules/@swc/cli": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/@swc/cli/-/cli-0.8.2.tgz", + "integrity": "sha512-4RAfbHx1oz/9zFutxIjm7famgfQeQ6s3Tc5rIWxXfB+QlA5y5rHKFi8NDCZ5iqZuZkMZTzMAHoWRRlioLO5i7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@swc/counter": "^0.1.3", + "@xhmikosr/bin-wrapper": "^14.0.0", + "commander": "^8.3.0", + "minimatch": "^9.0.3", + "piscina": "^4.3.1", + "semver": "^7.3.8", + "slash": "3.0.0", + "source-map": "^0.7.3", + "tinyglobby": "^0.2.13", + "write-file-atomic": "^7.0.1" + }, + "bin": { + "spack": "bin/spack.js", + "swc": "bin/swc.js", + "swcx": "bin/swcx.js" + }, + "engines": { + "node": ">= 20.19.0" + }, + "peerDependencies": { + "@swc/core": "^1.2.66", + "chokidar": "^5.0.0" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@swc/cli/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@swc/cli/node_modules/brace-expansion": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.7.tgz", + "integrity": "sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@swc/cli/node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/@swc/cli/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@swc/cli/node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "node_modules/@swc/core": { + "version": "1.16.21", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.16.21.tgz", + "integrity": "sha512-sE2asan0US6gcp11oBDFJb5+s+ltaAPBcagVQGub1+qo+8TsQET2TmXfJCw/Oy0w1eEPp1rV1wsR/Umx2KXGcw==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3", + "@swc/types": "^0.1.28" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/swc" + }, + "optionalDependencies": { + "@swc/core-darwin-arm64": "1.16.21", + "@swc/core-darwin-x64": "1.16.21", + "@swc/core-linux-arm-gnueabihf": "1.16.21", + "@swc/core-linux-arm64-gnu": "1.16.21", + "@swc/core-linux-arm64-musl": "1.16.21", + "@swc/core-linux-ppc64-gnu": "1.16.21", + "@swc/core-linux-s390x-gnu": "1.16.21", + "@swc/core-linux-x64-gnu": "1.16.21", + "@swc/core-linux-x64-musl": "1.16.21", + "@swc/core-win32-arm64-msvc": "1.16.21", + "@swc/core-win32-ia32-msvc": "1.16.21", + "@swc/core-win32-x64-msvc": "1.16.21" + }, + "peerDependencies": { + "@swc/helpers": ">=0.5.17" + }, + "peerDependenciesMeta": { + "@swc/helpers": { + "optional": true + } + } + }, + "node_modules/@swc/core-darwin-arm64": { + "version": "1.16.21", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.16.21.tgz", + "integrity": "sha512-VRreJpbRygpSD1hj3HBEOIA1xjXYnhB+IfcKsxZRhCIBGRP4UkqxZH4/ipFh/heIRp7EKQ56E8ygwEftLNCWXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-darwin-x64": { + "version": "1.16.21", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.16.21.tgz", + "integrity": "sha512-KqCJ04pMYDBUYpAOFGUFR8TEjqo4h0Vtwe/za9xNRDKsxSDnjgM7HVhfO0uNNpMMlM2ao2E6d0iLpApIRvNGnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm-gnueabihf": { + "version": "1.16.21", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.16.21.tgz", + "integrity": "sha512-tLkw3uWxQj69qBRX5Xm8vbq+5iX5e/ktoPXhrDeFFa2jNMc+AWcxa0bqwatvVGK40v5mcmz6Ulu5Nk9jF8ciMA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-gnu": { + "version": "1.16.21", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.16.21.tgz", + "integrity": "sha512-lFHVlHB0B5C7FwyprWJQp7b2iF/ohj2RqnMiv8uCnFJc+rYP0Py2I7ftZ+rOX4Zu7Ra9eMk7GnmuxAywCZYUJQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-musl": { + "version": "1.16.21", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.16.21.tgz", + "integrity": "sha512-gXDgrMMDsWT7r5gZsml10snBS9yaoVYwU9vGffa+DAts4ctEMrMHv+G8AH+ZxCV9gr5MfdgtGGQKjXjLay4mXQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-ppc64-gnu": { + "version": "1.16.21", + "resolved": "https://registry.npmjs.org/@swc/core-linux-ppc64-gnu/-/core-linux-ppc64-gnu-1.16.21.tgz", + "integrity": "sha512-hZF7J2YzXiADZ4eQxTFOWJcSSy3fbUR8/DcCXqiBHh0bVyLl2Y+O5pVWxk/r/XV5tA9n7DnnLlvkPPsHoajE9w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-s390x-gnu": { + "version": "1.16.21", + "resolved": "https://registry.npmjs.org/@swc/core-linux-s390x-gnu/-/core-linux-s390x-gnu-1.16.21.tgz", + "integrity": "sha512-rmtGwkAlbl+fQMGrGUMklUmByueOmt1ju9sKsE5YWVA0KfFYM0W87WVx4lFYQXt+E4P3zkLjeqo/AUa5mFbF1A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-gnu": { + "version": "1.16.21", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.16.21.tgz", + "integrity": "sha512-EUJsyrFw3aNE64iQzk3bDqjADgl4l0vpxfwVMjMuZIooNg35yKqvkw3bYNEhQyrBDY0+5kKoiirGLpth3Wf3lA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-musl": { + "version": "1.16.21", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.16.21.tgz", + "integrity": "sha512-ZXUnLjxTHxvNhVmQmYlb8XNKArfi/MiGoW5DmfuME8ttCLH9OJTqJVFy7g1yYxUCgw3tU6dF1uU59N8P/Yr5Pw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-arm64-msvc": { + "version": "1.16.21", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.16.21.tgz", + "integrity": "sha512-jnnQpAHiWZgEobVfOPvJJzm1Jh4ZEIKh2IqcDqHv6zJcjvaEkHdkt+qdRsJ9AqvK7g18WBRHTnvKm5IxdbkagA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-ia32-msvc": { + "version": "1.16.21", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.16.21.tgz", + "integrity": "sha512-TaVTnNm9c1I+VG3hJUt11uspwbTrTQ9+fUPzODlvx4tj/ChYqUm4cKTIHw3O97hILxpmhBR0hbvLEs9seOV59A==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-x64-msvc": { + "version": "1.16.21", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.16.21.tgz", + "integrity": "sha512-aPv58Clf6SdAQtouzDYKbEtcCETY2hOXlrHgSFnFIJQOlMWINHSGazaM1Iv63/9wOUDPq6+0ZFytMqnPtER9OQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@swc/types": { + "version": "0.1.28", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.28.tgz", + "integrity": "sha512-V6Mnml8v09QALx6K0elJ7o9K/MkVDtW3t6L+7Ou/JcWtb3xwId2AH4FeOceySd2JaO87IMw4+6vSZxLm34LPbw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3" + } + }, + "node_modules/@tokenizer/inflate": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.5.2.tgz", + "integrity": "sha512-3AZIuReWQ7i916s/UWAzy4V4k2wrkAZLW2sTi448W3w7Hz5uoc+Y7h+cnwzr0cwrwf3PsC5xKsH8MOl0NRltlg==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "strtok3": "^10.6.2", + "token-types": "^6.1.2" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@tokenizer/token": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", + "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", + "license": "MIT" + }, + "node_modules/@types/better-sqlite3": { + "version": "7.6.13", + "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz", + "integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cookiejar": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz", + "integrity": "sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@types/estree": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.10.tgz", "integrity": "sha512-lTyOMnOcmRBzdCv6/4zUZCrEhV0xrqQiUZBZtVcbmLHBXKLnN8q89v/wKRF/Q0HbwVsqbIG3o5HDoFl9sduM+g==", "dev": true, "license": "MIT" }, - "node_modules/@types/node": { - "version": "22.20.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.5.tgz", - "integrity": "sha512-U2+DNr+wSjpsTS/wZGYHq7GcwfuSmKiKvoPvK22zwTlRhU91yOniN4qRR5KhIjvif7ysw/dz/hKmfDH0Ris4aA==", + "node_modules/@types/express": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz", + "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "^2" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.3.tgz", + "integrity": "sha512-dPfW8NFiOF4wOHc7+N/QSxlY9cfSsenewGbAz8C8U/MULPd/YZ27LvJUIlzaXie7e6Ove9YunJGgC9tbHD2cKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@types/methods": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", + "integrity": "sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.19.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.19.2.tgz", + "integrity": "sha512-93d49wCWJkCyN87MxLpNN6KXPCwFw7bVUg79mGUV1OS40JGWwVCr8n61DXCSqz7CV6/VfPkb7xec7uP2M+QHWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": ">=7.24.0 <7.24.7" + } + }, + "node_modules/@types/qs": { + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*" + } + }, + "node_modules/@types/superagent": { + "version": "8.1.11", + "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-8.1.11.tgz", + "integrity": "sha512-KA7srSW/HENDtOw9DOqaFLgWuMqN9WgjEw62lh9dpvRaZDkhdOkazASd7X7i2eMUYLHa1U37ZttnePsH5zTDHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/cookiejar": "^2.1.5", + "@types/methods": "^1.1.4", + "@types/node": "*", + "form-data": "^4.0.0" + } + }, + "node_modules/@types/supertest": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/@types/supertest/-/supertest-7.2.1.tgz", + "integrity": "sha512-4CbBvoYVLHL7+yhbYrZET0vsvuyXTC05aRe7dNQkwMzm56auceoy6Yu3K50uZmwfHna1os3CMSgM/3QVkUtPTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/methods": "^1.1.4", + "@types/superagent": "^8.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.72.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.72.1.tgz", + "integrity": "sha512-fAuVMkwdO/huI8nyo/WKnNKf6Gag93S56Rh3Ts5mGcavwSdaN8tRg1gCeIGX8PF+nzp1BjGfO4gHhGzW1J6jTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.72.1", + "@typescript-eslint/types": "^8.72.1", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.72.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.72.1.tgz", + "integrity": "sha512-lC/t6eMCZZQAAKZEYGL6RDVSuFehpdmMT6h+nJIjIa6oylN/PwDgDVtEIbB7O9xFfZYtZAYLoLlDNWKJhtF+uQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.72.1", + "@typescript-eslint/visitor-keys": "8.72.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.72.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.72.1.tgz", + "integrity": "sha512-e4D5R5vpeAEPBxT2BAzsFQL3sbwyeeeo5+aczgNSRK1V734jE7pTqaGFU+fXapWEQwnZbpFaTn32+Z8DwS6OTg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.72.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.72.1.tgz", + "integrity": "sha512-LmE5IhQet1BWOWEXqBrVtBPsMAOQ6Nzk35d+WLh3mk4EUF33sqlfxjio1MdYeSvKSG5skactKKuixBIKXhL4Rw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.72.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.72.1.tgz", + "integrity": "sha512-DAlUoUV93hIvrtIQLwCbmZQYVZwOMhiHJrOVUUIWicFZaIYjTRqlY3I/NZevfkLmSqkNHRu2qYDcq9lrn16oOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.72.1", + "@typescript-eslint/tsconfig-utils": "8.72.1", + "@typescript-eslint/types": "8.72.1", + "@typescript-eslint/visitor-keys": "8.72.1", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.72.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.72.1.tgz", + "integrity": "sha512-5v5liypurJBFHCBNqh/RjsymAD+VWcE0djuGY/BUqp5fC3Qn+luWDEksjuTtWjPn6i5zQlUtkZCRG/CYG0yCkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.72.1", + "@typescript-eslint/types": "8.72.1", + "@typescript-eslint/typescript-estree": "8.72.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.72.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.72.1.tgz", + "integrity": "sha512-SB8xqipSzZHUePCCLNeYxqnxq83X61/qzNhjQGbTfjxGZIeOEP7tBgWYX05Tcd6CgE02WdTJnh5b8kMZJ/+bnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.72.1", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@vitest/expect": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.7.tgz", + "integrity": "sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.7", + "@vitest/utils": "3.2.7", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.7.tgz", + "integrity": "sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.7", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.7.tgz", + "integrity": "sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.7.tgz", + "integrity": "sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.7", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.7.tgz", + "integrity": "sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.7", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.7.tgz", + "integrity": "sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.7.tgz", + "integrity": "sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.7", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@xhmikosr/archive-type": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@xhmikosr/archive-type/-/archive-type-8.1.0.tgz", + "integrity": "sha512-EXOjEbnZFE5c/nFMf4FOrEURVanzHpnkPYmnmr78u02/8hAhE0FMq8p9TK1IM0/bFr5VcyBUY0gfLm8f7dKy+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "file-type": "^21.3.4" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@xhmikosr/archive-type/node_modules/@tokenizer/inflate": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.4.1.tgz", + "integrity": "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "token-types": "^6.1.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@xhmikosr/archive-type/node_modules/file-type": { + "version": "21.3.4", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-21.3.4.tgz", + "integrity": "sha512-Ievi/yy8DS3ygGvT47PjSfdFoX+2isQueoYP1cntFW1JLYAuS4GD7NUPGg4zv2iZfV52uDyk5w5Z0TdpRS6Q1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tokenizer/inflate": "^0.4.1", + "strtok3": "^10.3.4", + "token-types": "^6.1.1", + "uint8array-extras": "^1.4.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" + } + }, + "node_modules/@xhmikosr/bin-check": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@xhmikosr/bin-check/-/bin-check-8.3.0.tgz", + "integrity": "sha512-ALFOzArQWaPhSb6zr5yQslFr/vyc/ZJENLzFeFzqeTWI2exm3V84zMieU2FEI29KBuotkX1zf5RTKXIc0UaChQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "isexe": "^4.0.0", + "tinyexec": "^1.3.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@xhmikosr/bin-check/node_modules/isexe": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz", + "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=20" + } + }, + "node_modules/@xhmikosr/bin-check/node_modules/tinyexec": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.1.tgz", + "integrity": "sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@xhmikosr/bin-wrapper": { + "version": "14.5.2", + "resolved": "https://registry.npmjs.org/@xhmikosr/bin-wrapper/-/bin-wrapper-14.5.2.tgz", + "integrity": "sha512-I0hz3Y7V8jbHPXLRgAYOuHodl3/4eX+dDePRb8l+nwXY07Gog7Lt3cZ9tIz3IJIbtJZyocTTb8whsatPmr5Zjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@xhmikosr/bin-check": "^8.3.0", + "@xhmikosr/downloader": "^16.3.1", + "@xhmikosr/os-filter-obj": "^4.1.0", + "binary-version-check": "^6.1.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@xhmikosr/decompress": { + "version": "11.1.4", + "resolved": "https://registry.npmjs.org/@xhmikosr/decompress/-/decompress-11.1.4.tgz", + "integrity": "sha512-ZbYL7SAfY37/TMpopqBR3mQiuQ76kI/RNpN4q82YHSw/UxktZNy8P4wgiKPSrTImMAWRaUG8UK1pgEa56YdLaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@xhmikosr/decompress-tar": "^9.0.2", + "@xhmikosr/decompress-tarbz2": "^9.0.2", + "@xhmikosr/decompress-targz": "^9.0.1", + "@xhmikosr/decompress-unzip": "^8.2.1", + "graceful-fs": "^4.2.11", + "strip-dirs": "^3.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@xhmikosr/decompress-tar": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/@xhmikosr/decompress-tar/-/decompress-tar-9.0.2.tgz", + "integrity": "sha512-8nPZ6lZ3ExhsSxi/X/PMB3K+Vtsuxk43HowxYpxw4AsCHTYqFBXwC8B3Y+M/meaUOGOVm+2tFNUAfWGRjBt+Ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "file-type": "^21.3.4", + "is-stream": "^4.0.1", + "tar-stream": "3.1.7" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@xhmikosr/decompress-tar/node_modules/@tokenizer/inflate": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.4.1.tgz", + "integrity": "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "token-types": "^6.1.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@xhmikosr/decompress-tar/node_modules/file-type": { + "version": "21.3.4", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-21.3.4.tgz", + "integrity": "sha512-Ievi/yy8DS3ygGvT47PjSfdFoX+2isQueoYP1cntFW1JLYAuS4GD7NUPGg4zv2iZfV52uDyk5w5Z0TdpRS6Q1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tokenizer/inflate": "^0.4.1", + "strtok3": "^10.3.4", + "token-types": "^6.1.1", + "uint8array-extras": "^1.4.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" + } + }, + "node_modules/@xhmikosr/decompress-tar/node_modules/tar-stream": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", + "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "b4a": "^1.6.4", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, + "node_modules/@xhmikosr/decompress-tarbz2": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/@xhmikosr/decompress-tarbz2/-/decompress-tarbz2-9.0.2.tgz", + "integrity": "sha512-m0DvZhE7remCxtS8xY2iHSjivT4v+iyYDdfNoeuu8Nm+7g8xEXdLKSyDEicu4u1ImJLLGEfjMuTLera/F6UGWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@xhmikosr/decompress-tar": "^9.0.1", + "file-type": "^21.3.4", + "is-stream": "^4.0.1", + "seek-bzip": "^2.0.0", + "unbzip2-stream": "^1.4.3" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@xhmikosr/decompress-tarbz2/node_modules/@tokenizer/inflate": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.4.1.tgz", + "integrity": "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "token-types": "^6.1.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@xhmikosr/decompress-tarbz2/node_modules/file-type": { + "version": "21.3.4", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-21.3.4.tgz", + "integrity": "sha512-Ievi/yy8DS3ygGvT47PjSfdFoX+2isQueoYP1cntFW1JLYAuS4GD7NUPGg4zv2iZfV52uDyk5w5Z0TdpRS6Q1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tokenizer/inflate": "^0.4.1", + "strtok3": "^10.3.4", + "token-types": "^6.1.1", + "uint8array-extras": "^1.4.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" + } + }, + "node_modules/@xhmikosr/decompress-targz": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/@xhmikosr/decompress-targz/-/decompress-targz-9.0.1.tgz", + "integrity": "sha512-1JXu2b6yrpm5EuBoOzMU57B4qrHXJKWQQ7LlMynNEiz85mEjDciO3ayf//GXaTLLCEKiHjWlU3q3THjgf7uODA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@xhmikosr/decompress-tar": "^9.0.0", + "file-type": "^21.3.0", + "is-stream": "^4.0.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@xhmikosr/decompress-targz/node_modules/@tokenizer/inflate": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.4.1.tgz", + "integrity": "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "token-types": "^6.1.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@xhmikosr/decompress-targz/node_modules/file-type": { + "version": "21.3.4", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-21.3.4.tgz", + "integrity": "sha512-Ievi/yy8DS3ygGvT47PjSfdFoX+2isQueoYP1cntFW1JLYAuS4GD7NUPGg4zv2iZfV52uDyk5w5Z0TdpRS6Q1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tokenizer/inflate": "^0.4.1", + "strtok3": "^10.3.4", + "token-types": "^6.1.1", + "uint8array-extras": "^1.4.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" + } + }, + "node_modules/@xhmikosr/decompress-unzip": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/@xhmikosr/decompress-unzip/-/decompress-unzip-8.2.1.tgz", + "integrity": "sha512-2MS94QnmXQwjkKN8WyFiu1sU7J3rcWJcMze4kRYsX7tN+CXpUGECgkh4YSOhujpkWPuVlFudIziJHO/TxOqkQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "file-type": "^21.3.4", + "get-stream": "^9.0.1", + "yauzl": "^3.4.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@xhmikosr/decompress-unzip/node_modules/@tokenizer/inflate": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.4.1.tgz", + "integrity": "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "token-types": "^6.1.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@xhmikosr/decompress-unzip/node_modules/file-type": { + "version": "21.3.4", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-21.3.4.tgz", + "integrity": "sha512-Ievi/yy8DS3ygGvT47PjSfdFoX+2isQueoYP1cntFW1JLYAuS4GD7NUPGg4zv2iZfV52uDyk5w5Z0TdpRS6Q1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tokenizer/inflate": "^0.4.1", + "strtok3": "^10.3.4", + "token-types": "^6.1.1", + "uint8array-extras": "^1.4.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" + } + }, + "node_modules/@xhmikosr/downloader": { + "version": "16.3.1", + "resolved": "https://registry.npmjs.org/@xhmikosr/downloader/-/downloader-16.3.1.tgz", + "integrity": "sha512-M67dvznaFbsvoqhGT4FsHWysaXXQ8386OViGZm0WOyQS3apW9p16WgvHp9nWj2vfKQAR2ZdqIBPNCHSM5rKSCg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@xhmikosr/archive-type": "^8.1.0", + "@xhmikosr/decompress": "^11.1.4", + "content-disposition": "^2.0.1", + "ext-name": "^5.0.0", + "file-type": "^21.3.4", + "filenamify": "^7.0.2", + "got": "^14.6.6" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@xhmikosr/downloader/node_modules/@tokenizer/inflate": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.4.1.tgz", + "integrity": "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "token-types": "^6.1.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@xhmikosr/downloader/node_modules/file-type": { + "version": "21.3.4", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-21.3.4.tgz", + "integrity": "sha512-Ievi/yy8DS3ygGvT47PjSfdFoX+2isQueoYP1cntFW1JLYAuS4GD7NUPGg4zv2iZfV52uDyk5w5Z0TdpRS6Q1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tokenizer/inflate": "^0.4.1", + "strtok3": "^10.3.4", + "token-types": "^6.1.1", + "uint8array-extras": "^1.4.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" + } + }, + "node_modules/@xhmikosr/os-filter-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@xhmikosr/os-filter-obj/-/os-filter-obj-4.1.0.tgz", + "integrity": "sha512-y5ArHvQ7BVule/+L9yE2nYMhceiJhgsqo58lOfnisQ7bg+Kjfmkgr7JBuVFiTkl+ErdShpp829QstZQyLugl8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "system-architecture": "^1.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.19.0.tgz", + "integrity": "sha512-oJlA3XiRm7Cyk6qFD2Jn8ak9B7jSy0qy00ADO3+8dpT0LSjFihQYv4C02LFCSYJV3Q37xYwwRy5m+IpIiUzqWw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansis": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/ansis/-/ansis-4.4.0.tgz", + "integrity": "sha512-9k3v7xcHwgdO/DruxGIg4HtjvlAZlcnsX/mzqUb1t3NkYnl9kK2UJ+Gq0io+vQf7iT//BD/HB/NBkUR1LWxoeA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + } + }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", + "license": "MIT" + }, + "node_modules/array-timsort": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array-timsort/-/array-timsort-1.0.3.tgz", + "integrity": "sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/b4a": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.9.0.tgz", + "integrity": "sha512-dpfcF9fDNR6++cthXR67iyhgqWy9CBouAvIWhIntzBG6cvK/cnIPiZQjBwi/ZqjjBEDGfoNDtmB0kTjroOJ3pQ==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "react-native-b4a": "*" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/bare-events": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.9.2.tgz", + "integrity": "sha512-AIPKioV7/Y/8KfZ3AAhjPJxLLbY49S64Ym5DakZlUg75qQiTgUq9hEJoEwa4eUezPUlXRy/i5NpsKvo9jgKmoA==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "bare-abort-controller": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + } + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/better-sqlite3": { + "version": "12.11.1", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.11.1.tgz", + "integrity": "sha512-dq9AtApgg5PGFtBzPFSBl3HZQjHok5gaQCM6zh2Yk0aSmDCs1CbnVI8/HgASQkNKsWFpseIO9beg5xxpYhbIfA==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + }, + "engines": { + "node": "20.x || 22.x || 23.x || 24.x || 25.x || 26.x" + } + }, + "node_modules/bignumber.js": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/binary-version": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/binary-version/-/binary-version-7.1.0.tgz", + "integrity": "sha512-Iy//vPc3ANPNlIWd242Npqc8MK0a/i4kVcHDlDA6HNMv5zMxz4ulIFhOSYJVKw/8AbHdHy0CnGYEt1QqSXxPsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^8.0.1", + "find-versions": "^6.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/binary-version-check": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/binary-version-check/-/binary-version-check-6.1.0.tgz", + "integrity": "sha512-REKdLKmuViV2WrtWXvNSiPX04KbIjfUV3Cy8batUeOg+FtmowavzJorfFhWq95cVJzINnL/44ixP26TrdJZACA==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-version": "^7.1.0", + "semver": "^7.6.0", + "semver-truncate": "^3.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/body-parser": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.4.0.tgz", + "integrity": "sha512-Ifk1KcLrxaGt1occQagQNFNNOqRt1LFuehEnHHzB8Z6cRgIxRU1TBaiIsUiCdA+gy5PL7Y1IX31DNKkRmxtXjQ==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^2.1.0", + "debug": "^4.4.3", + "http-errors": "^2.0.1", + "iconv-lite": "^0.7.3", + "on-finished": "^2.4.1", + "qs": "^6.16.1", + "raw-body": "^3.0.2", + "type-is": "^2.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/byte-counter": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/byte-counter/-/byte-counter-0.1.0.tgz", + "integrity": "sha512-jheRLVMeUKrDBjVw2O5+k4EvR4t9wtxHL+bo/LxfkxsVeuGMy3a5SEGgXdAFA4FSzTrU8rQXQIrsZ3oBq5a0pQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cacheable": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/cacheable/-/cacheable-2.5.0.tgz", + "integrity": "sha512-60cyAOytib/OzBw1JNSoSV/boK1AtHryDIjvVBk7XbN4ugfkM3+Sry7fEjNgPMGgOjuaZPAp8ruZ0Cxafwyq9g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@cacheable/memory": "^2.2.0", + "@cacheable/utils": "^2.5.0", + "hookified": "^1.15.0", + "keyv": "^5.6.0", + "qified": "^0.10.1" + } + }, + "node_modules/cacheable-lookup": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-7.0.0.tgz", + "integrity": "sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + } + }, + "node_modules/cacheable-request": { + "version": "13.0.19", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-13.0.19.tgz", + "integrity": "sha512-SVXGH037+Mo1aIMO5B2UcleR43FGjFdN+M8JObSyEoQ2Mn4CODRWx28gN5jiTF0n5ItsgtIZfyargMNs8GX4kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-cache-semantics": "^4.2.0", + "get-stream": "^9.0.1", + "http-cache-semantics": "^4.2.0", + "keyv": "^5.6.0", + "mimic-response": "^4.0.0", + "normalize-url": "^8.1.1", + "responselike": "^4.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/cacheable-request/node_modules/mimic-response": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-4.0.0.tgz", + "integrity": "sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/chardet": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.2.0.tgz", + "integrity": "sha512-rddelWYNPRrXq6PtNEN2S3f6t9ILzvqaN5pVgi4kqt9jHQaXIial9PznB5iSPVlQSLNaaH22ItWz3EJtQ10+OA==", + "dev": true, + "license": "MIT" + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^5.0.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, + "node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-spinners": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-3.4.0.tgz", + "integrity": "sha512-bXfOC4QcT1tKXGorxL3wbJm6XJPDqEnij2gQ2m7ESQuE+/z9YFIWnl/5RpTiKWbMq3EVKR4fRLJGn6DVfu0mpw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-table3": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.5.tgz", + "integrity": "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "string-width": "^4.2.0" + }, + "engines": { + "node": "10.* || >= 12.*" + }, + "optionalDependencies": { + "@colors/colors": "1.5.0" + } + }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-15.0.0.tgz", + "integrity": "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/comment-json": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/comment-json/-/comment-json-5.0.0.tgz", + "integrity": "sha512-uiqLcOiVDJtBP8WGkZHEP+FZIhTzP1dxvn59EfoYUi9gqupjrBWVQkO2atDrbnKPwLeotFYDsuNb26uBMqB+hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-timsort": "^1.0.3", + "esprima": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/component-emitter": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", + "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/content-disposition": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-2.0.1.tgz", + "integrity": "sha512-e+H0ZXHSWYrENhQzw1LPuP4oF5MzVKmDU6d3hxlvaPEYLLg62MxtQNPRx4SYSuYJSBUgnQIG4HIN2tEtNv7Dog==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", + "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/convert-hrtime": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/convert-hrtime/-/convert-hrtime-5.0.0.tgz", + "integrity": "sha512-lOETlkIeYSJWcbbcvjRKGxVMXJR+8+OQb/mTPbA4ObPMytYIsUbuOE0Jzy60hjARYszq1id0j8KgVhC+WGZVTg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cookiejar": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", + "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", + "dev": true, + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dezalgo": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", + "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", + "dev": true, + "license": "ISC", + "dependencies": { + "asap": "^2.0.0", + "wrappy": "1" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/emojilib": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/emojilib/-/emojilib-2.4.0.tgz", + "integrity": "sha512-5U0rVMU5Y2n2+ykNLQqMoqklN9ICBT/KsvC1Gz6vqHbz2AXXGkG+Pm5rMWk/8Vjrr/mY9985Hi8DYzn1F09Nyw==", + "dev": true, + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.1.tgz", + "integrity": "sha512-Ev/hIyePLZtpL98x562lvPtenrUfGr0lfaf44HFZ+hDEwso7ccV9QAZrEVLg0hxNAzygaAFxy2dYaAKKM0G06A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "10.14.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.14.0.tgz", + "integrity": "sha512-T0pNsxkb9IKmtQjhpK3d7nWOpFv+YHuMbTJnSVMoQblF0onvXfErrtp4V1jHtsQPan7dKaGVD0q1WCXnzMF31A==", + "dev": true, + "license": "MIT", + "peer": true, + "workspaces": [ + "packages/*" + ], + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.5", + "@eslint/config-helpers": "^0.7.0", + "@eslint/core": "^1.2.1", + "@eslint/plugin-kit": "^0.7.3", + "@humanfs/node": "^0.16.8", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "11.1.5 || >11.1.6 <12", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "minimatch": "^10.2.6", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", + "dev": true, + "license": "BSD-2-Clause", + "peer": true, + "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/espree": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", + "dev": true, + "license": "BSD-2-Clause", + "peer": true, + "dependencies": { + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "peer": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/events-universal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", + "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.7.0" + } + }, + "node_modules/execa": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/execa/node_modules/get-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/execa/node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/execa/node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/express": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/express/-/express-5.3.0.tgz", + "integrity": "sha512-qCqy1xSoaugxOD4IVHb1ZWdkvCF2W3aE2okKouWj8VwJ+ztskSiXoBnCSJqfKT+cVPJhuJqe9YLbY425+Zh00g==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.3.0", + "content-disposition": "^2.0.1", + "content-type": "^2.0.0", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.8", + "qs": "^6.16.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.1.0", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ext-list": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/ext-list/-/ext-list-2.2.2.tgz", + "integrity": "sha512-u+SQgsubraE6zItfVA0tBuCBhfU9ogSRnsvygI7wht9TS510oLkBRXBsqopeUG/GBOIQyKZO9wjTqIu/sf5zFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "^1.28.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ext-name": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ext-name/-/ext-name-5.0.0.tgz", + "integrity": "sha512-yblEwXAbGv1VQDmow7s38W77hzAgJAO50ztBLMcUyUBfxv1HC+LGwtiEN+Co6LtlqT/5uwVOxsD4TNIilWhwdQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ext-list": "^2.0.0", + "sort-keys-length": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "license": "MIT" + }, + "node_modules/fast-string-truncated-width": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fast-string-truncated-width/-/fast-string-truncated-width-3.0.3.tgz", + "integrity": "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-string-width": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/fast-string-width/-/fast-string-width-3.0.2.tgz", + "integrity": "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-string-truncated-width": "^3.0.2" + } + }, + "node_modules/fast-uri": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.9.tgz", + "integrity": "sha512-hS/0Wt3Ya043oKXs0sIxrMpKQITXhMLIrVAKKXi4gozGC9v7YRWVnXIZzjNPTO02M9BdxLCWqK1yRXgNziVyJQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fast-wrap-ansi": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/fast-wrap-ansi/-/fast-wrap-ansi-0.2.2.tgz", + "integrity": "sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-string-width": "^3.0.2" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "node_modules/file-entry-cache": { + "version": "11.1.5", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-11.1.5.tgz", + "integrity": "sha512-+PFTHITI08JIGhnNpGNI8T8inUpgZfk3GNEqfT9R2zZV2iFXg3CvqzSl/uEhs7TSGujYRELEANyDvS8Fj7+S7Q==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "flat-cache": "^6.1.23" + } + }, + "node_modules/file-type": { + "version": "22.2.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-22.2.0.tgz", + "integrity": "sha512-XmEVJsRpQMVYC+sWFW+FKAVo2P7/lPctDdWH4UQ1r6oxhdrTobnowdM3z+ZJ93a4J8D3yDpxX0bEvRuFev0vUQ==", + "license": "MIT", + "dependencies": { + "@tokenizer/inflate": "^0.5.1", + "strtok3": "^10.5.0", + "token-types": "^6.1.2", + "uint8array-extras": "^1.6.0" + }, + "engines": { + "node": ">=22" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" + } + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" + }, + "node_modules/filename-reserved-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/filename-reserved-regex/-/filename-reserved-regex-4.1.0.tgz", + "integrity": "sha512-pdgqcVlu789LoYmu0kz4pxSphL0pX/UVFFDJ/kTHk+1ZRc43UiokPZFsRV8OGxW49khh11rj54AhqczGIwLa+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/filenamify": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/filenamify/-/filenamify-7.0.3.tgz", + "integrity": "sha512-Pf0dwHrWs1GcIK15ps304fmxp2AOcfNR6vOIMvCPEy6ZztKYxzRQHlOuPVWo2HRjbCLHaRTY+kD8n3RbQ0A/fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "filename-reserved-regex": "^4.0.1" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/find-versions": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/find-versions/-/find-versions-6.0.0.tgz", + "integrity": "sha512-2kCCtc+JvcZ86IGAz3Z2Y0A1baIz9fL31pH/0S1IqZr9Iwnjq8izfPtrCyQKO6TLMPELLsQMre7VDqeIKCsHkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver-regex": "^4.0.5", + "super-regex": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "6.1.23", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-6.1.23.tgz", + "integrity": "sha512-f++BY9pTk+983xK1FLzlLpmM0i0z+jHmx3QESGkURMXujQZz1k5wzwX6hjnQ8goaD0B+sYnDK1yZ6MTyZfUaqA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "cacheable": "^2.5.0", + "flatted": "^3.4.2", + "hookified": "^1.15.0" + } + }, + "node_modules/flatted": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", + "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", + "dev": true, + "license": "ISC", + "peer": true + }, + "node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/form-data-encoder": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-4.1.0.tgz", + "integrity": "sha512-G6NsmEW15s0Uw9XnCg+33H3ViYRyiM0hMrMhhqQOR8NFc5GhYrI+6I3u7OTw7b91J2g8rtvMBZJDbcGb2YUniw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18" + } + }, + "node_modules/form-data/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/form-data/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/formidable": { + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.4.tgz", + "integrity": "sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@paralleldrive/cuid2": "^2.2.2", + "dezalgo": "^1.0.4", + "once": "^1.4.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "url": "https://ko-fi.com/tunnckoCore/commissions" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function-timeout": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/function-timeout/-/function-timeout-1.0.2.tgz", + "integrity": "sha512-939eZS4gJ3htTHAldmyyuzlrD58P03fHG49v2JfFXbV6OhvZKRC9j2yAtdHw/zrp2zXHuv05zMIy40F0ge7spA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gaxios": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-8.3.2.tgz", + "integrity": "sha512-7VsaTExxjbDv2ZK1YuC4jbgfN33NI/6/7zdYWUHB6mcg51v4tCBZe+xzjqPzOpVzI+C5k1Kk9OQ80WVrXAHOqw==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/gcp-metadata": { + "version": "9.2.3", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-9.2.3.tgz", + "integrity": "sha512-d2OI/dJJ3IIaeOz3c9jvr5j5G5beI5fQzbgilNiWhDKGwvJN9o10Qub70NkbF6PNJSUoNKZ77JKYGz0Fs4+9Lg==", + "license": "Apache-2.0", + "dependencies": { + "gaxios": "^8.3.1", + "google-logging-utils": "^2.1.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.7.0.tgz", + "integrity": "sha512-XjH1AECxf0giL2V1aU8vKyRR2ppRUb5c0EvT7zuJTokQ74bNo52zOtghqdWIqrhUD79fo3x0WfKZdOqxF6LG1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz", + "integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sec-ant/readable-stream": "^0.4.1", + "is-stream": "^4.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/google-auth-library": { + "version": "11.3.2", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-11.3.2.tgz", + "integrity": "sha512-rbm+5boOZzFvDFg2tvCGkgHtG+RrmDqCDi6voK+ILpoCDqGxxc5pcMH6WZzCQXGNqQqZcYJl5WNvb159rkriuA==", + "license": "Apache-2.0", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^8.3.1", + "gcp-metadata": "^9.2.1", + "google-logging-utils": "^2.1.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/google-logging-utils": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-2.1.1.tgz", + "integrity": "sha512-jYZOPMaRGS3Hldyudft6bdibW9ZWUKBkMdB2TeSETkV1c2T9TR4zxEWZETHv9xJ8KHr2hgkNLaSa8kcEcwlpeQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=22" + } + }, + "node_modules/googleapis": { + "version": "189.0.0", + "resolved": "https://registry.npmjs.org/googleapis/-/googleapis-189.0.0.tgz", + "integrity": "sha512-TmFQmaLNcY/aaGNC9wZW9GvPueWB/fdNmt6eNFLKo5nrAxod4OAnvQNRxZuGO8iKObie5g3iLJloHQ8iiiIlvA==", + "license": "Apache-2.0", + "dependencies": { + "google-auth-library": "^11.0.0", + "googleapis-common": "^9.0.0" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/googleapis-common": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/googleapis-common/-/googleapis-common-9.3.1.tgz", + "integrity": "sha512-wtv/e7JYgmtpIPcSez2EFD/Wx49DPsRX2rj15D7hTwc7tDsP82dcwoX/I4+zE38mu6S9PcghizzzADLf+ZMunA==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "gaxios": "^8.3.0", + "google-auth-library": "^11.0.0", + "google-logging-utils": "^2.0.0", + "qs": "^6.7.0", + "url-template": "^2.0.8" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/got": { + "version": "14.6.6", + "resolved": "https://registry.npmjs.org/got/-/got-14.6.6.tgz", + "integrity": "sha512-QLV1qeYSo5l13mQzWgP/y0LbMr5Plr5fJilgAIwgnwseproEbtNym8xpLsDzeZ6MWXgNE6kdWGBjdh3zT/Qerg==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~6.21.0" + "@sindresorhus/is": "^7.0.1", + "byte-counter": "^0.1.0", + "cacheable-lookup": "^7.0.0", + "cacheable-request": "^13.0.12", + "decompress-response": "^10.0.0", + "form-data-encoder": "^4.0.2", + "http2-wrapper": "^2.2.1", + "keyv": "^5.5.3", + "lowercase-keys": "^3.0.0", + "p-cancelable": "^4.0.1", + "responselike": "^4.0.2", + "type-fest": "^4.26.1" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" } }, - "node_modules/acorn": { - "version": "8.19.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.19.0.tgz", - "integrity": "sha512-oJlA3XiRm7Cyk6qFD2Jn8ak9B7jSy0qy00ADO3+8dpT0LSjFihQYv4C02LFCSYJV3Q37xYwwRy5m+IpIiUzqWw==", + "node_modules/got/node_modules/@sindresorhus/is": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-7.2.0.tgz", + "integrity": "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==", "dev": true, "license": "MIT", - "bin": { - "acorn": "bin/acorn" + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/got/node_modules/decompress-response": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-10.0.0.tgz", + "integrity": "sha512-oj7KWToJuuxlPr7VV0vabvxEIiqNMo+q0NueIiL3XhtwC6FVOX7Hr1c0C4eD0bmf7Zr+S/dSf2xvkH3Ad6sU3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^4.0.0" }, "engines": { - "node": ">=0.4.0" + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/agent-base": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "node_modules/got/node_modules/mimic-response": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-4.0.0.tgz", + "integrity": "sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg==", + "dev": true, "license": "MIT", "engines": { - "node": ">= 14" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/any-promise": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", - "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "dev": true, - "license": "MIT" + "license": "ISC" }, - "node_modules/base64-js": { + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hashery": { "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" + "resolved": "https://registry.npmjs.org/hashery/-/hashery-1.5.1.tgz", + "integrity": "sha512-iZyKG96/JwPz1N55vj2Ie2vXbhu440zfUfJvSwEqEbeLluk7NnapfGqa7LH0mOsnDxTF85Mx8/dyR6HfqcbmbQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "hookified": "^1.15.0" + }, + "engines": { + "node": ">=20" + } }, - "node_modules/better-sqlite3": { - "version": "12.8.0", - "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.8.0.tgz", - "integrity": "sha512-RxD2Vd96sQDjQr20kdP+F+dK/1OUNiVOl200vKBZY8u0vTwysfolF6Hq+3ZK2+h8My9YvZhHsF+RSGZW2VYrPQ==", - "hasInstallScript": true, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", "license": "MIT", "dependencies": { - "bindings": "^1.5.0", - "prebuild-install": "^7.1.1" + "function-bind": "^1.1.2" }, "engines": { - "node": "20.x || 22.x || 23.x || 24.x || 25.x" + "node": ">= 0.4" } }, - "node_modules/bignumber.js": { - "version": "9.3.1", - "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", - "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "node_modules/helmet": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/helmet/-/helmet-8.3.0.tgz", + "integrity": "sha512-Qgpiaws3Sm30Av8Eah6sjMCZZwjlBu+E68rhpCWBshY1lb09HtLwj5GviX0OyQIn+ulUS0iX0AxN5n3tLZzz1w==", "license": "MIT", "engines": { - "node": "*" + "node": ">=18.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/EvanHahn" } }, - "node_modules/bindings": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", - "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "node_modules/hookified": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/hookified/-/hookified-1.15.1.tgz", + "integrity": "sha512-MvG/clsADq1GPM2KGo2nyfaWVyn9naPiXrqIe4jYjXNZQt238kWyOGrsyc/DmRAQ+Re6yeo6yX/yoNCG5KAEVg==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/http-cache-semantics": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.3.0.tgz", + "integrity": "sha512-M5t5LlJpS1UHMjvwRQVdFHvPISGeLAxNcrWuJkeGh0KxsqCHZ1O3NXZU/8x7cD0BDcGW8kapxMKTvwlqrNkHkA==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", "license": "MIT", "dependencies": { - "file-uri-to-path": "1.0.0" + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/bl": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "node_modules/http2-wrapper": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-2.2.4.tgz", + "integrity": "sha512-y5jx4isTOKEC5Hu1lswc+IxytGlqDmwF1qJtk9qIS8l1KVxuHO5g522UfmErt1H2pMcH/xq+KiFQUB8aVWIABA==", + "dev": true, "license": "MIT", "dependencies": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.2.0" + }, + "engines": { + "node": ">=10.19.0" } }, - "node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/human-signals": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=16.17.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", + "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", "funding": [ { "type": "github", @@ -1035,155 +6073,321 @@ "url": "https://feross.org/support" } ], + "license": "BSD-3-Clause" + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, + "node_modules/inspect-with-kind": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/inspect-with-kind/-/inspect-with-kind-1.0.5.tgz", + "integrity": "sha512-MAQUJuIo7Xqk8EVNP+6d3CKq9c80hi4tjIbIAT6lmGW9W6WzlHiu9PS8uSuUYU+Do+j1baiFp3H25XEVxDIG2g==", + "dev": true, + "license": "ISC", "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" + "kind-of": "^6.0.2" } }, - "node_modules/buffer-equal-constant-time": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", - "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", - "license": "BSD-3-Clause" + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } }, - "node_modules/bundle-require": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/bundle-require/-/bundle-require-5.1.0.tgz", - "integrity": "sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==", + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "dev": true, "license": "MIT", - "dependencies": { - "load-tsconfig": "^0.2.3" - }, + "peer": true, "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "peerDependencies": { - "esbuild": ">=0.18" + "node": ">=0.10.0" } }, - "node_modules/cac": { - "version": "6.7.14", - "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", - "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true, "license": "MIT", "engines": { "node": ">=8" } }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, "license": "MIT", + "peer": true, "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" + "is-extglob": "^2.1.1" }, "engines": { - "node": ">= 0.4" + "node": ">=0.10.0" } }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "node_modules/is-interactive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", + "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", + "dev": true, "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - }, "engines": { - "node": ">= 0.4" + "node": ">=12" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/chokidar": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", - "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "node_modules/is-plain-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", "dev": true, "license": "MIT", - "dependencies": { - "readdirp": "^4.0.1" + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/is-stream": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz", + "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "dev": true, + "license": "MIT", "engines": { - "node": ">= 14.16.0" + "node": ">=18" }, "funding": { - "url": "https://paulmillr.com/funding/" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/chownr": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, "license": "ISC" }, - "node_modules/commander": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", - "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "node_modules/iterare": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/iterare/-/iterare-1.2.1.tgz", + "integrity": "sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q==", + "license": "ISC", + "engines": { + "node": ">=6" + } + }, + "node_modules/jose": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/jose/-/jose-5.10.0.tgz", + "integrity": "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", "dev": true, + "license": "MIT" + }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", "license": "MIT", - "engines": { - "node": ">= 6" + "dependencies": { + "bignumber.js": "^9.0.0" } }, - "node_modules/confbox": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", - "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "dev": true, "license": "MIT" }, - "node_modules/consola": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", - "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "dev": true, "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, "engines": { - "node": "^14.18.0 || >=16.10.0" + "node": ">=6" } }, - "node_modules/data-uri-to-buffer": { + "node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { "version": "4.0.1", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", - "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/keyv": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz", + "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@keyv/serialize": "^1.1.1" + } + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, "license": "MIT", "engines": { - "node": ">= 12" + "node": ">=0.10.0" } }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, "license": "MIT", + "peer": true, "dependencies": { - "ms": "^2.1.3" + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" }, "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true + "node": ">= 0.8.0" + } + }, + "node_modules/load-esm": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/load-esm/-/load-esm-1.0.3.tgz", + "integrity": "sha512-v5xlu8eHD1+6r8EHTg6hfmO97LN8ugKtiXcy5e6oN72iD2r6u0RPfLl6fxM+7Wnh2ZRq15o0russMst44WauPA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + }, + { + "type": "buymeacoffee", + "url": "https://buymeacoffee.com/borewit" } + ], + "license": "MIT", + "engines": { + "node": ">=13.2.0" } }, - "node_modules/decompress-response": { + "node_modules/load-tsconfig": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/load-tsconfig/-/load-tsconfig-0.2.5.tgz", + "integrity": "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/locate-path": { "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", - "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, "license": "MIT", + "peer": true, "dependencies": { - "mimic-response": "^3.1.0" + "p-locate": "^5.0.0" }, "engines": { "node": ">=10" @@ -1192,734 +6396,839 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/deep-extend": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "node_modules/log-symbols": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-7.0.1.tgz", + "integrity": "sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg==", + "dev": true, "license": "MIT", + "dependencies": { + "is-unicode-supported": "^2.0.0", + "yoctocolors": "^2.1.1" + }, "engines": { - "node": ">=4.0.0" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "license": "Apache-2.0", - "engines": { - "node": ">=8" - } + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "node_modules/lowercase-keys": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-3.0.0.tgz", + "integrity": "sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==", + "dev": true, "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, "engines": { - "node": ">= 0.4" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/ecdsa-sig-formatter": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", - "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", - "license": "Apache-2.0", + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", "dependencies": { - "safe-buffer": "^5.0.1" + "@jridgewell/sourcemap-codec": "^1.5.5" } }, - "node_modules/end-of-stream": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", - "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "node_modules/make-asynchronous": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/make-asynchronous/-/make-asynchronous-1.1.0.tgz", + "integrity": "sha512-ayF7iT+44LXdxJLTrTd3TLQpFDDvPCBxXxbv+pMUSuHA5Q8zyAfwkRP6aHHwNVFBUFWtxAHqwNJxF8vMZLAbVg==", + "dev": true, "license": "MIT", "dependencies": { - "once": "^1.4.0" + "p-event": "^6.0.0", + "type-fest": "^4.6.0", + "web-worker": "^1.5.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.1.tgz", + "integrity": "sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ==", "license": "MIT", "engines": { - "node": ">= 0.4" + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", "license": "MIT", "engines": { - "node": ">= 0.4" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/es-object-atoms": { + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/methods": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", - "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "dev": true, "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, "engines": { - "node": ">= 0.4" + "node": ">= 0.6" } }, - "node_modules/esbuild": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", - "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", "dev": true, - "hasInstallScript": true, "license": "MIT", "bin": { - "esbuild": "bin/esbuild" + "mime": "cli.js" }, "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.7", - "@esbuild/android-arm": "0.27.7", - "@esbuild/android-arm64": "0.27.7", - "@esbuild/android-x64": "0.27.7", - "@esbuild/darwin-arm64": "0.27.7", - "@esbuild/darwin-x64": "0.27.7", - "@esbuild/freebsd-arm64": "0.27.7", - "@esbuild/freebsd-x64": "0.27.7", - "@esbuild/linux-arm": "0.27.7", - "@esbuild/linux-arm64": "0.27.7", - "@esbuild/linux-ia32": "0.27.7", - "@esbuild/linux-loong64": "0.27.7", - "@esbuild/linux-mips64el": "0.27.7", - "@esbuild/linux-ppc64": "0.27.7", - "@esbuild/linux-riscv64": "0.27.7", - "@esbuild/linux-s390x": "0.27.7", - "@esbuild/linux-x64": "0.27.7", - "@esbuild/netbsd-arm64": "0.27.7", - "@esbuild/netbsd-x64": "0.27.7", - "@esbuild/openbsd-arm64": "0.27.7", - "@esbuild/openbsd-x64": "0.27.7", - "@esbuild/openharmony-arm64": "0.27.7", - "@esbuild/sunos-x64": "0.27.7", - "@esbuild/win32-arm64": "0.27.7", - "@esbuild/win32-ia32": "0.27.7", - "@esbuild/win32-x64": "0.27.7" + "node": ">=4.0.0" } }, - "node_modules/expand-template": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", - "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", - "license": "(MIT OR WTFPL)", + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", "engines": { - "node": ">=6" + "node": ">= 0.6" } }, - "node_modules/extend": { + "node_modules/mime-types": { "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "license": "MIT" + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } }, - "node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", "dev": true, "license": "MIT", "engines": { - "node": ">=12.0.0" + "node": ">=12" }, - "peerDependencies": { - "picomatch": "^3 || ^4" + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/fetch-blob": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", - "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "paypal", - "url": "https://paypal.me/jimmywarting" - } - ], + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "license": "BlueOak-1.0.0", "dependencies": { - "node-domexception": "^1.0.0", - "web-streams-polyfill": "^3.0.3" + "brace-expansion": "^5.0.8" }, "engines": { - "node": "^12.20 || >= 14.13" + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/file-uri-to-path": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", - "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", - "license": "MIT" - }, - "node_modules/fix-dts-default-cjs-exports": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/fix-dts-default-cjs-exports/-/fix-dts-default-cjs-exports-1.0.1.tgz", - "integrity": "sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==", - "dev": true, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", "license": "MIT", - "dependencies": { - "magic-string": "^0.30.17", - "mlly": "^1.7.4", - "rollup": "^4.34.8" + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/formdata-polyfill": { - "version": "4.0.10", - "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", - "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/multer": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/multer/-/multer-2.5.0.tgz", + "integrity": "sha512-BsqOzYGVq49JcW2IzoSAxeKYRevb+dGGPPWleOR8Lrlk4igwxu1/9pwSXlByhCb7MVZDrNJFC0IBP5/O5nLYRw==", "license": "MIT", "dependencies": { - "fetch-blob": "^3.1.2" + "append-field": "^1.0.0", + "busboy": "^1.6.0", + "type-is": "^1.6.18" }, "engines": { - "node": ">=12.20.0" + "node": ">= 10.16.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/fs-constants": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", - "license": "MIT" - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, + "node_modules/multer/node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + "node": ">= 0.6" } }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "node_modules/multer/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" + "engines": { + "node": ">= 0.6" } }, - "node_modules/gaxios": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-8.3.2.tgz", - "integrity": "sha512-7VsaTExxjbDv2ZK1YuC4jbgfN33NI/6/7zdYWUHB6mcg51v4tCBZe+xzjqPzOpVzI+C5k1Kk9OQ80WVrXAHOqw==", - "license": "Apache-2.0", + "node_modules/multer/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", "dependencies": { - "extend": "^3.0.2", - "https-proxy-agent": "^7.0.1", - "node-fetch": "^3.3.2" + "mime-db": "1.52.0" }, "engines": { - "node": ">=22" + "node": ">= 0.6" } }, - "node_modules/gcp-metadata": { - "version": "9.2.3", - "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-9.2.3.tgz", - "integrity": "sha512-d2OI/dJJ3IIaeOz3c9jvr5j5G5beI5fQzbgilNiWhDKGwvJN9o10Qub70NkbF6PNJSUoNKZ77JKYGz0Fs4+9Lg==", - "license": "Apache-2.0", + "node_modules/multer/node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", "dependencies": { - "gaxios": "^8.3.1", - "google-logging-utils": "^2.1.0", - "json-bigint": "^1.0.0" + "media-typer": "0.3.0", + "mime-types": "~2.1.24" }, "engines": { - "node": ">=22" + "node": ">= 0.6" } }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "node_modules/mute-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-3.0.0.tgz", + "integrity": "sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.20", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.20.tgz", + "integrity": "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" + "bin": { + "nanoid": "bin/nanoid.cjs" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/negotiator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.1.0.tgz", + "integrity": "sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg==", "license": "MIT", "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" + "content-type": "^2.1.0" }, "engines": { - "node": ">= 0.4" + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/github-from-package": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", - "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", - "license": "MIT" - }, - "node_modules/google-auth-library": { - "version": "11.3.2", - "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-11.3.2.tgz", - "integrity": "sha512-rbm+5boOZzFvDFg2tvCGkgHtG+RrmDqCDi6voK+ILpoCDqGxxc5pcMH6WZzCQXGNqQqZcYJl5WNvb159rkriuA==", - "license": "Apache-2.0", + "node_modules/node-abi": { + "version": "3.101.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.101.0.tgz", + "integrity": "sha512-bkb/RkuWCE64eWu1PCh3Fu2pGc9mKRs+kOE+FU2bGHRYlQa86/D321DSATV54AsCQMKz3/xSt9sRgraZTpqdhQ==", + "license": "MIT", "dependencies": { - "base64-js": "^1.3.0", - "ecdsa-sig-formatter": "^1.0.11", - "gaxios": "^8.3.1", - "gcp-metadata": "^9.2.1", - "google-logging-utils": "^2.1.0", - "jws": "^4.0.0" + "semver": "^7.3.5" }, "engines": { - "node": ">=22" + "node": ">=10" } }, - "node_modules/google-logging-utils": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-2.1.1.tgz", - "integrity": "sha512-jYZOPMaRGS3Hldyudft6bdibW9ZWUKBkMdB2TeSETkV1c2T9TR4zxEWZETHv9xJ8KHr2hgkNLaSa8kcEcwlpeQ==", - "license": "Apache-2.0", + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", "engines": { - "node": ">=22" + "node": ">=10.5.0" } }, - "node_modules/googleapis": { - "version": "189.0.0", - "resolved": "https://registry.npmjs.org/googleapis/-/googleapis-189.0.0.tgz", - "integrity": "sha512-TmFQmaLNcY/aaGNC9wZW9GvPueWB/fdNmt6eNFLKo5nrAxod4OAnvQNRxZuGO8iKObie5g3iLJloHQ8iiiIlvA==", - "license": "Apache-2.0", + "node_modules/node-emoji": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-2.2.0.tgz", + "integrity": "sha512-Z3lTE9pLaJF47NyMhd4ww1yFTAP8YhYI8SleJiHzM46Fgpm5cnNzSl9XfzFNqbaz+VlJrIj3fXQ4DeN1Rjm6cw==", + "dev": true, + "license": "MIT", "dependencies": { - "google-auth-library": "^11.0.0", - "googleapis-common": "^9.0.0" + "@sindresorhus/is": "^4.6.0", + "char-regex": "^1.0.2", + "emojilib": "^2.4.0", + "skin-tone": "^2.0.0" }, "engines": { - "node": ">=22.0.0" + "node": ">=18" } }, - "node_modules/googleapis-common": { - "version": "9.3.1", - "resolved": "https://registry.npmjs.org/googleapis-common/-/googleapis-common-9.3.1.tgz", - "integrity": "sha512-wtv/e7JYgmtpIPcSez2EFD/Wx49DPsRX2rj15D7hTwc7tDsP82dcwoX/I4+zE38mu6S9PcghizzzADLf+ZMunA==", - "license": "Apache-2.0", + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", "dependencies": { - "extend": "^3.0.2", - "gaxios": "^8.3.0", - "google-auth-library": "^11.0.0", - "google-logging-utils": "^2.0.0", - "qs": "^6.7.0", - "url-template": "^2.0.8" + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" }, "engines": { - "node": ">=22" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" } }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "node_modules/normalize-url": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.1.1.tgz", + "integrity": "sha512-JYc0DPlpGWB40kH5g07gGTrYuMqV653k3uBKY6uITPWds3M0ov3GaWGp9lbE3Bzngx8+XkfzgvASb9vk9JDFXQ==", + "dev": true, "license": "MIT", "engines": { - "node": ">= 0.4" + "node": ">=14.16" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "node_modules/npm-run-path": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", + "dev": true, "license": "MIT", + "dependencies": { + "path-key": "^4.0.0" + }, "engines": { - "node": ">= 0.4" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/hasown": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", - "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "node_modules/npm-run-path/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" + "engines": { + "node": ">=12" }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", "engines": { - "node": ">= 0.4" + "node": ">=0.10.0" } }, - "node_modules/hono": { - "version": "4.13.16", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.13.16.tgz", - "integrity": "sha512-AOLOEhvrV+iH92xXCnFr/5+3KjPKcI3xP63XJtX5spAnkznPdHijlIsUTpi7/9EhWqrTCekw5c5zh1K+YsVw7A==", + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", "license": "MIT", "engines": { - "node": ">=16.9.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/https-proxy-agent": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "node_modules/on-finished": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.2.tgz", + "integrity": "sha512-ATWUv387i+nLD49EiDoOft5scf32Vsdz3ktrQJG9bi7jRFmaXTssvyvj1EmeKVVgvesbqUtvQQvaS2TpTukAZQ==", "license": "MIT", "dependencies": { - "agent-base": "^7.1.2", - "debug": "4" + "ee-first": "1.1.1" }, "engines": { - "node": ">= 14" + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "BSD-3-Clause" - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC" - }, - "node_modules/ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "license": "ISC" + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } }, - "node_modules/joycon": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", - "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", + "node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", "dev": true, "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, "engines": { - "node": ">=10" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/json-bigint": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", - "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, "license": "MIT", + "peer": true, "dependencies": { - "bignumber.js": "^9.0.0" + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" } }, - "node_modules/jwa": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", - "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "node_modules/ora": { + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-9.4.1.tgz", + "integrity": "sha512-6VlU9MLXbjVQD04AZCMX28hVtA5bUoadvUqO76MUCVA0ilwJbMiHsITRPfyVm6p/BC0Av/BXMujx39WCe1LEqw==", + "dev": true, "license": "MIT", "dependencies": { - "buffer-equal-constant-time": "^1.0.1", - "ecdsa-sig-formatter": "1.0.11", - "safe-buffer": "^5.0.1" + "chalk": "^5.6.2", + "cli-cursor": "^5.0.0", + "cli-spinners": "^3.2.0", + "is-interactive": "^2.0.0", + "is-unicode-supported": "^2.1.0", + "log-symbols": "^7.0.1", + "stdin-discarder": "^0.3.2", + "string-width": "^8.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/jws": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", - "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "node_modules/ora/node_modules/ansi-regex": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.5.0.tgz", + "integrity": "sha512-zhvbbbdt1kbw7EJ9OQoxmQcuaNR9P5kTj7e/YdmOSU10aHWbycofJxA6jKfoEy3vlLdctOF9ZHaxLtmSm998wQ==", + "dev": true, "license": "MIT", - "dependencies": { - "jwa": "^2.0.1", - "safe-buffer": "^5.0.1" + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, - "node_modules/lilconfig": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.4.tgz", - "integrity": "sha512-zJZyBmQhArDRefuWTypZxwDpFDIYmrj8ypSBvwGD5P13Y95s6DjzKgu4DVdGbeQa5PDEsAOCAPNk4X+QOR006w==", + "node_modules/ora/node_modules/string-width": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.3.0.tgz", + "integrity": "sha512-ZbmZM0JCihQN91dWnxoipT2KOEyHqEyfRXUyjuRhW8b/xnqPDoq4gWEVApTVa9db2wN8mmoikgFBbjh71+cGeQ==", "dev": true, "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" + }, "engines": { - "node": ">=14" + "node": ">=20" }, "funding": { - "url": "https://github.com/sponsors/antonk52" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true, - "license": "MIT" - }, - "node_modules/load-tsconfig": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/load-tsconfig/-/load-tsconfig-0.2.5.tgz", - "integrity": "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==", + "node_modules/ora/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", "dev": true, "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "node_modules/magic-string": { - "version": "0.30.21", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", - "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "node_modules/oxfmt": { + "version": "0.75.0", + "resolved": "https://registry.npmjs.org/oxfmt/-/oxfmt-0.75.0.tgz", + "integrity": "sha512-/ZKvY5asfsVi9hdkOmRIumqOPu6kxlTgiJSZf54uq29kKew+Gu1FFAnEkGePyvLVW+MAYu41D6XggnzoElQzPw==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.5" + "tinypool": "2.2.0" + }, + "bin": { + "oxfmt": "bin/oxfmt" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/sponsors/oxc-project" + }, + "optionalDependencies": { + "@oxfmt/binding-android-arm-eabi": "0.75.0", + "@oxfmt/binding-android-arm64": "0.75.0", + "@oxfmt/binding-darwin-arm64": "0.75.0", + "@oxfmt/binding-darwin-x64": "0.75.0", + "@oxfmt/binding-freebsd-x64": "0.75.0", + "@oxfmt/binding-linux-arm-gnueabihf": "0.75.0", + "@oxfmt/binding-linux-arm-musleabihf": "0.75.0", + "@oxfmt/binding-linux-arm64-gnu": "0.75.0", + "@oxfmt/binding-linux-arm64-musl": "0.75.0", + "@oxfmt/binding-linux-ppc64-gnu": "0.75.0", + "@oxfmt/binding-linux-riscv64-gnu": "0.75.0", + "@oxfmt/binding-linux-riscv64-musl": "0.75.0", + "@oxfmt/binding-linux-s390x-gnu": "0.75.0", + "@oxfmt/binding-linux-x64-gnu": "0.75.0", + "@oxfmt/binding-linux-x64-musl": "0.75.0", + "@oxfmt/binding-openharmony-arm64": "0.75.0", + "@oxfmt/binding-win32-arm64-msvc": "0.75.0", + "@oxfmt/binding-win32-ia32-msvc": "0.75.0", + "@oxfmt/binding-win32-x64-msvc": "0.75.0" + }, + "peerDependencies": { + "@astrojs/compiler-rs": "^0.5.0", + "svelte": "^5.0.0", + "vite-plus": "*" + }, + "peerDependenciesMeta": { + "@astrojs/compiler-rs": { + "optional": true + }, + "svelte": { + "optional": true + }, + "vite-plus": { + "optional": true + } } }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "node_modules/oxfmt/node_modules/tinypool": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-2.2.0.tgz", + "integrity": "sha512-jBrmx4lYmaC9k/mgPbylxs7kBUxHtD8256up+HjLaDFfXScKJQyil+SWXSvhAtT7XHo+yTVlpB81PGmHP8oLSQ==", + "dev": true, "license": "MIT", "engines": { - "node": ">= 0.4" + "node": "^20.0.0 || >=22.0.0" } }, - "node_modules/mimic-response": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "node_modules/oxlint": { + "version": "1.90.0", + "resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.90.0.tgz", + "integrity": "sha512-RkTN9EmgIUpLuhNoLhO3OliMGJlvb3Dk7XVjBZnfoMCHrVpuz5ESd97klfiyYeV7XfA2lNLnDCPMkUI+Qw5How==", + "dev": true, "license": "MIT", + "bin": { + "oxlint": "bin/oxlint" + }, "engines": { - "node": ">=10" + "node": "^20.19.0 || >=22.12.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/oxc-project" + }, + "optionalDependencies": { + "@oxlint/binding-android-arm-eabi": "1.90.0", + "@oxlint/binding-android-arm64": "1.90.0", + "@oxlint/binding-darwin-arm64": "1.90.0", + "@oxlint/binding-darwin-x64": "1.90.0", + "@oxlint/binding-freebsd-x64": "1.90.0", + "@oxlint/binding-linux-arm-gnueabihf": "1.90.0", + "@oxlint/binding-linux-arm-musleabihf": "1.90.0", + "@oxlint/binding-linux-arm64-gnu": "1.90.0", + "@oxlint/binding-linux-arm64-musl": "1.90.0", + "@oxlint/binding-linux-ppc64-gnu": "1.90.0", + "@oxlint/binding-linux-riscv64-gnu": "1.90.0", + "@oxlint/binding-linux-riscv64-musl": "1.90.0", + "@oxlint/binding-linux-s390x-gnu": "1.90.0", + "@oxlint/binding-linux-x64-gnu": "1.90.0", + "@oxlint/binding-linux-x64-musl": "1.90.0", + "@oxlint/binding-openharmony-arm64": "1.90.0", + "@oxlint/binding-win32-arm64-msvc": "1.90.0", + "@oxlint/binding-win32-ia32-msvc": "1.90.0", + "@oxlint/binding-win32-x64-msvc": "1.90.0" + }, + "peerDependencies": { + "oxlint-tsgolint": ">=7.0.2005", + "vite-plus": "*" + }, + "peerDependenciesMeta": { + "oxlint-tsgolint": { + "optional": true + }, + "vite-plus": { + "optional": true + } } }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "node_modules/p-cancelable": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-4.0.1.tgz", + "integrity": "sha512-wBowNApzd45EIKdO1LaU+LrMBwAcjfPaYtVzV3lmfM3gf8Z4CHZsiIqlM8TZZ8okYvh5A1cP6gTfCRQtwUpaUg==", + "dev": true, "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" + "engines": { + "node": ">=14.16" } }, - "node_modules/mkdirp-classic": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", - "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", - "license": "MIT" - }, - "node_modules/mlly": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz", - "integrity": "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==", + "node_modules/p-event": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/p-event/-/p-event-6.0.1.tgz", + "integrity": "sha512-Q6Bekk5wpzW5qIyUP4gdMEujObYstZl6DMMOSenwBvV0BlE5LkDwkjs5yHbZmdCEq2o4RJx4tE1vwxFVf2FG1w==", "dev": true, "license": "MIT", "dependencies": { - "acorn": "^8.16.0", - "pathe": "^2.0.3", - "pkg-types": "^1.3.1", - "ufo": "^1.6.3" + "p-timeout": "^6.1.2" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/mz": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", - "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { - "any-promise": "^1.0.0", - "object-assign": "^4.0.1", - "thenify-all": "^1.0.0" + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/napi-build-utils": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", - "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", - "license": "MIT" - }, - "node_modules/node-abi": { - "version": "3.101.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.101.0.tgz", - "integrity": "sha512-bkb/RkuWCE64eWu1PCh3Fu2pGc9mKRs+kOE+FU2bGHRYlQa86/D321DSATV54AsCQMKz3/xSt9sRgraZTpqdhQ==", + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, "license": "MIT", + "peer": true, "dependencies": { - "semver": "^7.3.5" + "p-limit": "^3.0.2" }, "engines": { "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/node-domexception": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", - "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", - "deprecated": "Use your platform's native DOMException instead", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "github", - "url": "https://paypal.me/jimmywarting" - } - ], + "node_modules/p-timeout": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-6.1.4.tgz", + "integrity": "sha512-MyIV3ZA/PmyBN/ud8vV9XzwTrNtR4jFrObymZYnZqMmW0zA8Z17vnT0rBgFE/TlohB+YCHqXMgZzb3Csp49vqg==", + "dev": true, "license": "MIT", "engines": { - "node": ">=10.5.0" + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/node-fetch": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", - "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", "license": "MIT", - "dependencies": { - "data-uri-to-buffer": "^4.0.0", - "fetch-blob": "^3.1.4", - "formdata-polyfill": "^4.0.10" - }, "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/node-fetch" + "node": ">= 0.8" } }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", "dev": true, "license": "MIT", + "peer": true, "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, - "node_modules/object-inspect": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, "license": "MIT", "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "license": "ISC", - "dependencies": { - "wrappy": "1" + "node": ">=8" } }, - "node_modules/openai": { - "version": "7.47.0", - "resolved": "https://registry.npmjs.org/openai/-/openai-7.47.0.tgz", - "integrity": "sha512-BYa2RYgocAv9WjPUJmNu8fICuA1fFhkTBEpiR3sC5L1K6v8Hnhc8pcnQEhu8l/+W1XKA+JUaBa7HpT+AK1s77w==", - "license": "Apache-2.0", - "engines": { - "node": ">=22.0.0" - }, - "peerDependencies": { - "@aws-sdk/credential-provider-node": ">=3.972.0 <4", - "@smithy/hash-node": ">=4.3.0 <5", - "@smithy/signature-v4": ">=5.4.0 <6", - "google-auth-library": ">=9.15.1 <12", - "undici": ">=5 <9", - "ws": "^8.21.0", - "zod": "^3.25 || ^4.0" - }, - "peerDependenciesMeta": { - "@aws-sdk/credential-provider-node": { - "optional": true - }, - "@smithy/hash-node": { - "optional": true - }, - "@smithy/signature-v4": { - "optional": true - }, - "google-auth-library": { - "optional": true - }, - "undici": { - "optional": true - }, - "ws": { - "optional": true - }, - "zod": { - "optional": true - } + "node_modules/path-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/pathe": { @@ -1929,6 +7238,23 @@ "dev": true, "license": "MIT" }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true, + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -1949,38 +7275,40 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/pirates": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", - "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "node_modules/piscina": { + "version": "4.9.4", + "resolved": "https://registry.npmjs.org/piscina/-/piscina-4.9.4.tgz", + "integrity": "sha512-RyBDr2VheQ8ZfH3N8SzQZHztqVyOtadTwLnUYof6gdj5161/eu2wNhCbGl5GHnNKDpnkicJYpKtL2J+y/gk6XA==", "dev": true, "license": "MIT", - "engines": { - "node": ">= 6" + "optionalDependencies": { + "@napi-rs/nice": "^1.0.1" } }, - "node_modules/pkg-types": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", - "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", + "node_modules/pluralize": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", + "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", "dev": true, "license": "MIT", - "dependencies": { - "confbox": "^0.1.8", - "mlly": "^1.7.4", - "pathe": "^2.0.1" + "engines": { + "node": ">=4" } }, - "node_modules/postcss-load-config": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", - "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "node_modules/postcss": { + "version": "8.5.32", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.32.tgz", + "integrity": "sha512-LJlKCCAUFKOuZxipUdYw1NzIITEsPcgyOG9LkvRJvuKj/bRzbuRY0ARXvL0pWGpsZvn8jku+LMep2eU0seHp8g==", "dev": true, "funding": [ { "type": "opencollective", "url": "https://opencollective.com/postcss/" }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, { "type": "github", "url": "https://github.com/sponsors/ai" @@ -1988,30 +7316,12 @@ ], "license": "MIT", "dependencies": { - "lilconfig": "^3.1.1" + "nanoid": "^3.3.20", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.2" }, "engines": { - "node": ">= 18" - }, - "peerDependencies": { - "jiti": ">=1.21.0", - "postcss": ">=8.0.9", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "jiti": { - "optional": true - }, - "postcss": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } + "node": "^10 || ^12 || >=14" } }, "node_modules/prebuild-install": { @@ -2041,6 +7351,34 @@ "node": ">=10" } }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.8.tgz", + "integrity": "sha512-5nnx0yGyVUcY6t9RnWcARWtwT9F1D8O9rt08htPvnd49W1IgZtmLkhu9WfMzQj1cFxjHIO6connUNVW5k7AVyQ==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/pump": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", @@ -2051,20 +7389,94 @@ "once": "^1.3.1" } }, - "node_modules/qs": { - "version": "6.17.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.17.1.tgz", - "integrity": "sha512-jd3COkaKVgGUW+ZHyEHCG9bH7RoaaiWuMgzsIBjUMVjLAaWGWpoUAVwljVvBHxRu/2qEHnadLGZrkutYUBPQJA==", - "license": "BSD-3-Clause", + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/qified": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/qified/-/qified-0.10.1.tgz", + "integrity": "sha512-+Owyggi9IxT1ePKGafcI87ubSmxol6smwJ+RAHDQlx9+9cPwFWDiKFFCPuWhr9ignlGpZ9vDQLw67N4dcTVFEA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "hookified": "^2.1.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/qified/node_modules/hookified": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/hookified/-/hookified-2.2.0.tgz", + "integrity": "sha512-p/LgFzRN5FeoD3DLS6bkUapeye6E4SI6yJs6KetENd18S+FBthqYq2amJUWpt5z0EQwwHemidjY5OqJGEKm5uA==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/qs": { + "version": "6.17.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.17.1.tgz", + "integrity": "sha512-jd3COkaKVgGUW+ZHyEHCG9bH7RoaaiWuMgzsIBjUMVjLAaWGWpoUAVwljVvBHxRu/2qEHnadLGZrkutYUBPQJA==", + "license": "BSD-3-Clause", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/range-parser": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.3.0.tgz", + "integrity": "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", "dependencies": { - "es-define-property": "^1.0.1", - "side-channel": "^1.1.1" + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" }, "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">= 0.10" } }, "node_modules/rc": { @@ -2097,27 +7509,73 @@ } }, "node_modules/readdirp": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", - "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.1.1.tgz", + "integrity": "sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA==", "dev": true, "license": "MIT", "engines": { - "node": ">= 14.18.0" + "node": ">= 20.19.0" }, "funding": { "type": "individual", "url": "https://paulmillr.com/funding/" } }, - "node_modules/resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "node_modules/reflect-metadata": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", + "license": "Apache-2.0" + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", "dev": true, "license": "MIT", "engines": { - "node": ">=8" + "node": ">=0.10.0" + } + }, + "node_modules/resolve-alpn": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/responselike": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-4.0.2.tgz", + "integrity": "sha512-cGk8IbWEAnaCpdAt1BHzJ3Ahz5ewDJa0KseTsE3qIRMJ3C698W8psM7byCeWVpd/Ha7FUYzuRVzXoKoM6nRUbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "lowercase-keys": "^3.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/rollup": { @@ -2167,6 +7625,31 @@ "fsevents": "~2.3.2" } }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -2187,6 +7670,36 @@ ], "license": "MIT" }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/seek-bzip": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/seek-bzip/-/seek-bzip-2.0.0.tgz", + "integrity": "sha512-SMguiTnYrhpLdk3PwfzHeotrcwi8bNV4iemL9tx9poR/yeaMYwB9VzR1w7b57DuWpuqR8n6oZboi0hj3AxZxQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "^6.0.0" + }, + "bin": { + "seek-bunzip": "bin/seek-bunzip", + "seek-table": "bin/seek-bzip-table" + } + }, + "node_modules/seek-bzip/node_modules/commander": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", + "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/semver": { "version": "7.8.5", "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", @@ -2199,6 +7712,109 @@ "node": ">=10" } }, + "node_modules/semver-regex": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/semver-regex/-/semver-regex-4.0.5.tgz", + "integrity": "sha512-hunMQrEy1T6Jr2uEVjrAIqjwWcQTgOAcIM52C8MY1EZSD3DDNft04XzvYKPqjED65bNVVko0YI38nYeEHCX3yw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/semver-truncate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/semver-truncate/-/semver-truncate-3.0.0.tgz", + "integrity": "sha512-LJWA9kSvMolR51oDE6PN3kALBNaUdkxzAGcexw8gjMA8xr5zUqK0JiR3CgARSqanYF3Z1YHvsErb1KDgh+v7Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/side-channel": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", @@ -2271,6 +7887,26 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/simple-concat": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", @@ -2316,23 +7952,200 @@ "simple-concat": "^1.0.0" } }, + "node_modules/skin-tone": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/skin-tone/-/skin-tone-2.0.0.tgz", + "integrity": "sha512-kUMbT1oBJCpgrnKoSr0o6wPtvRWT9W9UKvGLwfJYO2WuahZRHOpEyL1ckyMGgMWh0UdpmaoFqKKD29WTomNEGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "unicode-emoji-modifier-base": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/sort-keys": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz", + "integrity": "sha512-vzn8aSqKgytVik0iwdBEi+zevbTYZogewTUM6dtpmGwEcdzbub/TX4bCzRhebDCRC3QzXgJsLRKB2V/Oof7HXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-plain-obj": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sort-keys-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/sort-keys-length/-/sort-keys-length-1.0.1.tgz", + "integrity": "sha512-GRbEOUqCxemTAk/b32F2xa8wDTs+Z1QHOkbhJDQTvv/6G3ZkbJ+frYWsTcc7cBB3Fu4wy4XlLCuNtJuMn7Gsvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "sort-keys": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map": { - "version": "0.7.6", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", - "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0.tgz", + "integrity": "sha512-d8EqvL+k/SOXCreS/SUzg2ciyHqBBLcN/yuRjFsbvVhHTE2pgei7oAhmPM7kWFbkX6OSMQfUq4KbkF3au9lhYQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "node_modules/source-map-js": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", + "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/stdin-discarder": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.3.2.tgz", + "integrity": "sha512-eCPu1qRxPVkl5605OTWF8Wz40b4Mf45NY5LQmVPQ599knfs5QhASUm9GbJ5BDMDOXgrnh0wyEdvzmL//YMlw0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/streamx": { + "version": "2.28.1", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.28.1.tgz", + "integrity": "sha512-zEzXb0s5Cds7tqMH6rhZ05lcJydCWiQPEwiNngVqzsxCc962vLY4Uw+mW7od8kDH258k2Uz/JrOkdIAAhSh9VA==", + "dev": true, + "license": "MIT", + "dependencies": { + "events-universal": "^1.0.0", + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", "dev": true, - "license": "BSD-3-Clause", + "license": "MIT", "engines": { - "node": ">= 12" + "node": ">=4" } }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "license": "MIT", + "node_modules/strip-dirs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-dirs/-/strip-dirs-3.0.0.tgz", + "integrity": "sha512-I0sdgcFTfKQlUPZyAqPJmSG3HLO9rWDFnxonnIbskYNM3DwFOeTNB5KzVq3dA1GdRAc/25b5Y7UO2TQfKWw4aQ==", + "dev": true, + "license": "ISC", "dependencies": { - "safe-buffer": "~5.2.0" + "inspect-with-kind": "^1.0.5", + "is-plain-obj": "^1.1.0" + } + }, + "node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/strip-json-comments": { @@ -2344,27 +8157,100 @@ "node": ">=0.10.0" } }, - "node_modules/sucrase": { - "version": "3.35.1", - "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", - "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "node_modules/strip-literal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/gen-mapping": "^0.3.2", - "commander": "^4.0.0", - "lines-and-columns": "^1.1.6", - "mz": "^2.7.0", - "pirates": "^4.0.1", - "tinyglobby": "^0.2.11", - "ts-interface-checker": "^0.1.9" + "js-tokens": "^9.0.1" }, - "bin": { - "sucrase": "bin/sucrase", - "sucrase-node": "bin/sucrase-node" + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/strtok3": { + "version": "10.6.3", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.6.3.tgz", + "integrity": "sha512-MD8mENgyXMr2mDaDfzP3RWYDSyYQxIIou7HhCiCJiBekrsoDtUG0id3vYnjDfY9ndOEbazFXe13Qw6u/1AB8ew==", + "license": "MIT", + "dependencies": { + "@tokenizer/token": "^0.3.0" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/super-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/super-regex/-/super-regex-1.1.0.tgz", + "integrity": "sha512-WHkws2ZflZe41zj6AolvvmaTrWds/VuyeYr9iPVv/oQeaIoVxMKaushfFWpOGDT+GuBrM/sVqF8KUCYQlSSTdQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-timeout": "^1.0.1", + "make-asynchronous": "^1.0.1", + "time-span": "^5.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/superagent": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-10.4.1.tgz", + "integrity": "sha512-PVMkMrhKrSTtFhUU8jiWuGh/gyRMKiyCTCmm0+qVzRNxlfntmRKJkDhlEXN62x3HIb33sQ4kmL8lcSdccWAchQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "component-emitter": "^1.3.1", + "cookiejar": "^2.1.4", + "debug": "^4.3.7", + "fast-safe-stringify": "^2.1.1", + "form-data": "^4.0.5", + "formidable": "^3.5.4", + "methods": "^1.1.2", + "mime": "2.6.0", + "qs": "^6.14.1" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/supertest": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/supertest/-/supertest-7.3.1.tgz", + "integrity": "sha512-vNaNCFoCR/edDbErQZp5KLNT8R4dSPHX7rlDLoE8hWLAm/878j6ShMbe8HK/E6RwwuhlSXVelXAIg9AxR5WWPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cookie-signature": "^1.2.2", + "methods": "^1.1.2", + "superagent": "^10.3.0" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/system-architecture": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/system-architecture/-/system-architecture-1.0.0.tgz", + "integrity": "sha512-0OJWD12D7XX3KUg1DYkMaTTjSTo2k/mhIYI3HlBlceXSMcJhW/1qO735fPKS5prcyjvn57Ub151vvASYXpQrEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/tar-fs": { @@ -2395,29 +8281,46 @@ "node": ">=6" } }, - "node_modules/thenify": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", - "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "node_modules/text-decoder": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz", + "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==", "dev": true, - "license": "MIT", + "license": "Apache-2.0", "dependencies": { - "any-promise": "^1.0.0" + "b4a": "^1.6.4" } }, - "node_modules/thenify-all": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", - "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/time-span": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/time-span/-/time-span-5.1.0.tgz", + "integrity": "sha512-75voc/9G4rDIJleOo4jPvN4/YC4GRZrY8yy1uU4lwrB3XEQbWve8zXoO5No4eFrGcTAMYyoY67p8jRQdtA1HbA==", "dev": true, "license": "MIT", "dependencies": { - "thenify": ">= 3.1.0 < 4" + "convert-hrtime": "^5.0.0" }, "engines": { - "node": ">=0.8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, "node_modules/tinyexec": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", @@ -2442,99 +8345,426 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, - "node_modules/tree-kill": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", - "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", - "dev": true, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.6.tgz", + "integrity": "sha512-u8KszXvGfU68hVcZpRHKG28T0krMuv2G5nDhiHaMLen/gIuFEgIJhaJuO69qjnXg5paSrbPMFfx3brNuN8eVSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/token-types": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.1.2.tgz", + "integrity": "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==", + "license": "MIT", + "dependencies": { + "@borewit/text-codec": "^0.2.1", + "@tokenizer/token": "^0.3.0", + "ieee754": "^1.2.1" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/tsconfig-paths": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", + "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "json5": "^2.2.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", + "license": "MIT", + "dependencies": { + "content-type": "^2.0.0", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/uid": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/uid/-/uid-2.0.2.tgz", + "integrity": "sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g==", + "license": "MIT", + "dependencies": { + "@lukeed/csprng": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/uint8array-extras": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.6.0.tgz", + "integrity": "sha512-8iAasVS4wUx0gPLjH8Xtz2PeDzTSiy8QiLGu2DT3X5GU+egFEQhpnbtkehbAwjvDcxIERmCYcysiODFmE3Ud0Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/unbzip2-stream": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz", + "integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer": "^5.2.1", + "through": "^2.3.8" + } + }, + "node_modules/undici-types": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", + "dev": true, + "license": "MIT" + }, + "node_modules/unicode-emoji-modifier-base": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unicode-emoji-modifier-base/-/unicode-emoji-modifier-base-1.0.0.tgz", + "integrity": "sha512-yLSH4py7oFH3oG/9K+XWrz1pSi3dfUrWEnInbxMfArOfc1+33BlGPQtLsOYwvdMy11AwUBetYuaRxSPqgkq+8g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/unplugin": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-3.7.0.tgz", + "integrity": "sha512-GrM2uq3DQo3hvLU2FjeYhooJ8BY9N6j1Fbzgx7Pta3VVJevsD3VP4dPjZkZOS0mLvN8w9zzr9OlP+31bSMyuLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "@rolldown/pluginutils": "^1.0.1", + "picomatch": "^4.0.7", + "webpack-virtual-modules": "^0.6.2" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@farmfe/core": "*", + "@rsbuild/core": "*", + "@rspack/core": "*", + "bun-types-no-globals": "*", + "esbuild": "*", + "rolldown": "*", + "rollup": "*", + "unloader": "*", + "vite": "*", + "webpack": "*" + }, + "peerDependenciesMeta": { + "@farmfe/core": { + "optional": true + }, + "@rsbuild/core": { + "optional": true + }, + "@rspack/core": { + "optional": true + }, + "bun-types-no-globals": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "rolldown": { + "optional": true + }, + "rollup": { + "optional": true + }, + "unloader": { + "optional": true + }, + "vite": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/unplugin-swc": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unplugin-swc/-/unplugin-swc-2.0.0.tgz", + "integrity": "sha512-hMdGJEtkwiJhzuXXx4Nb1XYDeD9KG4+P7KZOaErJTpZNYhTo1sbWIsXb+u410zXyaqH00veNX4UxOr6TpUwNiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.4.0", + "load-tsconfig": "^0.2.5", + "unplugin": "^3.3.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@swc/core": "^1.2.108" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "peer": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/url-template": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/url-template/-/url-template-2.0.8.tgz", + "integrity": "sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==", + "license": "BSD" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", "license": "MIT", - "bin": { - "tree-kill": "cli.js" + "engines": { + "node": ">= 0.8" } }, - "node_modules/ts-interface-checker": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", - "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/tsup": { - "version": "8.5.1", - "resolved": "https://registry.npmjs.org/tsup/-/tsup-8.5.1.tgz", - "integrity": "sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing==", + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", "dev": true, "license": "MIT", "dependencies": { - "bundle-require": "^5.1.0", - "cac": "^6.7.14", - "chokidar": "^4.0.3", - "consola": "^3.4.0", - "debug": "^4.4.0", - "esbuild": "^0.27.0", - "fix-dts-default-cjs-exports": "^1.0.0", - "joycon": "^3.1.1", - "picocolors": "^1.1.1", - "postcss-load-config": "^6.0.1", - "resolve-from": "^5.0.0", - "rollup": "^4.34.8", - "source-map": "^0.7.6", - "sucrase": "^3.35.0", - "tinyexec": "^0.3.2", - "tinyglobby": "^0.2.11", - "tree-kill": "^1.2.2" + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" }, "bin": { - "tsup": "dist/cli-default.js", - "tsup-node": "dist/cli-node.js" + "vite": "bin/vite.js" }, "engines": { - "node": ">=18" + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" }, "peerDependencies": { - "@microsoft/api-extractor": "^7.36.0", - "@swc/core": "^1", - "postcss": "^8.4.12", - "typescript": ">=4.5.0" + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" }, "peerDependenciesMeta": { - "@microsoft/api-extractor": { + "@types/node": { "optional": true }, - "@swc/core": { + "less": { "optional": true }, - "postcss": { + "lightningcss": { "optional": true }, - "typescript": { + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { "optional": true } } }, - "node_modules/tsx": { - "version": "4.23.15", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.15.tgz", - "integrity": "sha512-Yiex1Ovn8z2xPpOWckIiysV1SSyRMY9BkLF++q0yKiDxCqRhosKfMg3janKkiLBwZ5c/YryloKwGZcrEmtwxKw==", + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "~0.28.0" + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "bin": { - "tsx": "dist/cli.mjs" + "vite-node": "vite-node.mjs" }, "engines": { - "node": ">=18.0.0" + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" }, - "optionalDependencies": { - "fsevents": "~2.3.3" + "funding": { + "url": "https://opencollective.com/vitest" } }, - "node_modules/tsx/node_modules/@esbuild/aix-ppc64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", - "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", + "node_modules/vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", "cpu": [ "ppc64" ], @@ -2545,13 +8775,13 @@ "aix" ], "engines": { - "node": ">=18" + "node": ">=12" } }, - "node_modules/tsx/node_modules/@esbuild/android-arm": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", - "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", "cpu": [ "arm" ], @@ -2562,13 +8792,13 @@ "android" ], "engines": { - "node": ">=18" + "node": ">=12" } }, - "node_modules/tsx/node_modules/@esbuild/android-arm64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", - "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", "cpu": [ "arm64" ], @@ -2579,13 +8809,13 @@ "android" ], "engines": { - "node": ">=18" + "node": ">=12" } }, - "node_modules/tsx/node_modules/@esbuild/android-x64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", - "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", "cpu": [ "x64" ], @@ -2596,13 +8826,13 @@ "android" ], "engines": { - "node": ">=18" + "node": ">=12" } }, - "node_modules/tsx/node_modules/@esbuild/darwin-arm64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", - "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", "cpu": [ "arm64" ], @@ -2613,13 +8843,13 @@ "darwin" ], "engines": { - "node": ">=18" + "node": ">=12" } }, - "node_modules/tsx/node_modules/@esbuild/darwin-x64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", - "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", "cpu": [ "x64" ], @@ -2630,13 +8860,13 @@ "darwin" ], "engines": { - "node": ">=18" + "node": ">=12" } }, - "node_modules/tsx/node_modules/@esbuild/freebsd-arm64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", - "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", "cpu": [ "arm64" ], @@ -2647,13 +8877,13 @@ "freebsd" ], "engines": { - "node": ">=18" + "node": ">=12" } }, - "node_modules/tsx/node_modules/@esbuild/freebsd-x64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", - "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", "cpu": [ "x64" ], @@ -2664,13 +8894,13 @@ "freebsd" ], "engines": { - "node": ">=18" + "node": ">=12" } }, - "node_modules/tsx/node_modules/@esbuild/linux-arm": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", - "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", "cpu": [ "arm" ], @@ -2681,13 +8911,13 @@ "linux" ], "engines": { - "node": ">=18" + "node": ">=12" } }, - "node_modules/tsx/node_modules/@esbuild/linux-arm64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", - "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", "cpu": [ "arm64" ], @@ -2698,13 +8928,13 @@ "linux" ], "engines": { - "node": ">=18" + "node": ">=12" } }, - "node_modules/tsx/node_modules/@esbuild/linux-ia32": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", - "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", "cpu": [ "ia32" ], @@ -2715,13 +8945,13 @@ "linux" ], "engines": { - "node": ">=18" + "node": ">=12" } }, - "node_modules/tsx/node_modules/@esbuild/linux-loong64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", - "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", "cpu": [ "loong64" ], @@ -2732,13 +8962,13 @@ "linux" ], "engines": { - "node": ">=18" + "node": ">=12" } }, - "node_modules/tsx/node_modules/@esbuild/linux-mips64el": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", - "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", "cpu": [ "mips64el" ], @@ -2749,13 +8979,13 @@ "linux" ], "engines": { - "node": ">=18" + "node": ">=12" } }, - "node_modules/tsx/node_modules/@esbuild/linux-ppc64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", - "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", "cpu": [ "ppc64" ], @@ -2766,13 +8996,13 @@ "linux" ], "engines": { - "node": ">=18" + "node": ">=12" } }, - "node_modules/tsx/node_modules/@esbuild/linux-riscv64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", - "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", "cpu": [ "riscv64" ], @@ -2783,13 +9013,13 @@ "linux" ], "engines": { - "node": ">=18" + "node": ">=12" } }, - "node_modules/tsx/node_modules/@esbuild/linux-s390x": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", - "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", "cpu": [ "s390x" ], @@ -2800,13 +9030,13 @@ "linux" ], "engines": { - "node": ">=18" + "node": ">=12" } }, - "node_modules/tsx/node_modules/@esbuild/linux-x64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", - "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", "cpu": [ "x64" ], @@ -2817,30 +9047,13 @@ "linux" ], "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/netbsd-arm64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", - "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" + "node": ">=12" } }, - "node_modules/tsx/node_modules/@esbuild/netbsd-x64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", - "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", "cpu": [ "x64" ], @@ -2851,30 +9064,13 @@ "netbsd" ], "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/openbsd-arm64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", - "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" + "node": ">=12" } }, - "node_modules/tsx/node_modules/@esbuild/openbsd-x64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", - "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", "cpu": [ "x64" ], @@ -2885,30 +9081,13 @@ "openbsd" ], "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/openharmony-arm64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", - "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" + "node": ">=12" } }, - "node_modules/tsx/node_modules/@esbuild/sunos-x64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", - "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", "cpu": [ "x64" ], @@ -2919,13 +9098,13 @@ "sunos" ], "engines": { - "node": ">=18" + "node": ">=12" } }, - "node_modules/tsx/node_modules/@esbuild/win32-arm64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", - "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", "cpu": [ "arm64" ], @@ -2936,13 +9115,13 @@ "win32" ], "engines": { - "node": ">=18" + "node": ">=12" } }, - "node_modules/tsx/node_modules/@esbuild/win32-ia32": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", - "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", "cpu": [ "ia32" ], @@ -2953,13 +9132,13 @@ "win32" ], "engines": { - "node": ">=18" + "node": ">=12" } }, - "node_modules/tsx/node_modules/@esbuild/win32-x64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", - "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", "cpu": [ "x64" ], @@ -2970,13 +9149,13 @@ "win32" ], "engines": { - "node": ">=18" + "node": ">=12" } }, - "node_modules/tsx/node_modules/esbuild": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", - "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", + "node_modules/vite/node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -2984,96 +9163,172 @@ "esbuild": "bin/esbuild" }, "engines": { - "node": ">=18" + "node": ">=12" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.28.2", - "@esbuild/android-arm": "0.28.2", - "@esbuild/android-arm64": "0.28.2", - "@esbuild/android-x64": "0.28.2", - "@esbuild/darwin-arm64": "0.28.2", - "@esbuild/darwin-x64": "0.28.2", - "@esbuild/freebsd-arm64": "0.28.2", - "@esbuild/freebsd-x64": "0.28.2", - "@esbuild/linux-arm": "0.28.2", - "@esbuild/linux-arm64": "0.28.2", - "@esbuild/linux-ia32": "0.28.2", - "@esbuild/linux-loong64": "0.28.2", - "@esbuild/linux-mips64el": "0.28.2", - "@esbuild/linux-ppc64": "0.28.2", - "@esbuild/linux-riscv64": "0.28.2", - "@esbuild/linux-s390x": "0.28.2", - "@esbuild/linux-x64": "0.28.2", - "@esbuild/netbsd-arm64": "0.28.2", - "@esbuild/netbsd-x64": "0.28.2", - "@esbuild/openbsd-arm64": "0.28.2", - "@esbuild/openbsd-x64": "0.28.2", - "@esbuild/openharmony-arm64": "0.28.2", - "@esbuild/sunos-x64": "0.28.2", - "@esbuild/win32-arm64": "0.28.2", - "@esbuild/win32-ia32": "0.28.2", - "@esbuild/win32-x64": "0.28.2" - } - }, - "node_modules/tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", - "license": "Apache-2.0", + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/vitest": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.7.tgz", + "integrity": "sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg==", + "dev": true, + "license": "MIT", "dependencies": { - "safe-buffer": "^5.0.1" + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.7", + "@vitest/mocker": "3.2.7", + "@vitest/pretty-format": "^3.2.7", + "@vitest/runner": "3.2.7", + "@vitest/snapshot": "3.2.7", + "@vitest/spy": "3.2.7", + "@vitest/utils": "3.2.7", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" }, "engines": { - "node": "*" + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.7", + "@vitest/ui": "3.2.7", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } } }, - "node_modules/typescript": { - "version": "5.6.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", - "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "license": "MIT", "engines": { - "node": ">=14.17" + "node": ">= 8" } }, - "node_modules/ufo": { - "version": "1.6.4", - "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.4.tgz", - "integrity": "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==", + "node_modules/web-worker": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/web-worker/-/web-worker-1.5.0.tgz", + "integrity": "sha512-RiMReJrTAiA+mBjGONMnjVDP2u3p9R1vkcGz6gDIrOMT3oGuYwX2WRMYI9ipkphSuE5XKEhydbhNEJh4NY9mlw==", "dev": true, - "license": "MIT" + "license": "Apache-2.0" }, - "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "node_modules/webpack-virtual-modules": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", + "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==", "dev": true, "license": "MIT" }, - "node_modules/url-template": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/url-template/-/url-template-2.0.8.tgz", - "integrity": "sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==", - "license": "BSD" + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "license": "MIT" + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } }, - "node_modules/web-streams-polyfill": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", - "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, "license": "MIT", + "peer": true, "engines": { - "node": ">= 8" + "node": ">=0.10.0" } }, "node_modules/wrappy": { @@ -3082,6 +9337,59 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "license": "ISC" }, + "node_modules/write-file-atomic": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-7.0.1.tgz", + "integrity": "sha512-OTIk8iR8/aCRWBqvxrzxR0hgxWpnYBblY1S5hDWBQfk/VFmJwzmJgQFN3WsoUKHISv2eAwe+PpbUzyL1CKTLXg==", + "dev": true, + "license": "ISC", + "dependencies": { + "signal-exit": "^4.0.1" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/yauzl": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-3.4.0.tgz", + "integrity": "sha512-jIH9yLR9wqr0wOS0TpBvo/g/2UgZH5qePVbjgRliiF0BYvOZyaBknKsF+x9Iht0O6sqgnB93rCICdOZFecJuDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yoctocolors": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.2.0.tgz", + "integrity": "sha512-xYqdZFUK/VYazNl/oCDYN+3WloWQwMfZxBoiNt6qNyk+xfOdi598muWE42rNZFp1kNOiqW936q5RhUdnpqElSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/zod": { "version": "4.6.7", "resolved": "https://registry.npmjs.org/zod/-/zod-4.6.7.tgz", diff --git a/api/package.json b/api/package.json index 15a315b..60551df 100644 --- a/api/package.json +++ b/api/package.json @@ -1,26 +1,50 @@ { "name": "finkit-api", "version": "2.0.0", - "type": "module", "scripts": { - "dev": "tsx watch src/index.ts", - "build": "tsup src/index.ts --format esm --dts --clean", - "start": "node dist/index.js", - "postinstall": "node scripts/install-better-sqlite3.cjs || npm rebuild better-sqlite3" + "dev": "nest start --watch", + "predev": "node scripts/install-better-sqlite3.cjs", + "build": "node ./node_modules/@nestjs/cli/bin/nest.js build", + "start": "node dist/main.js", + "prestart": "node scripts/install-better-sqlite3.cjs", + "test": "vitest run", + "postinstall": "node scripts/install-better-sqlite3.cjs", + "lint": "oxlint", + "lint:fix": "oxlint --fix", + "format": "oxfmt", + "format:check": "oxfmt --check", + "check": "tsc -p tsconfig.build.json --noEmit && oxlint && oxfmt --check" }, "dependencies": { - "@hono/node-server": "^2.1.5", - "better-sqlite3": "12.8.0", - "googleapis": "^189.0.0", - "hono": "^4.13.16", - "openai": "^7.47.0", - "zod": "^4.6.7" + "@nestjs/common": "12.3.2", + "@nestjs/core": "12.3.2", + "@nestjs/platform-express": "12.3.2", + "better-sqlite3": "12.11.1", + "googleapis": "189.0.0", + "helmet": "8.3.0", + "jose": "5.10.0", + "reflect-metadata": "0.2.2", + "rxjs": "7.8.2", + "zod": "4.6.7" }, "devDependencies": { - "@types/better-sqlite3": "^7.6.13", - "@types/node": "^22.10.0", - "tsup": "^8.5.1", - "tsx": "^4.23.15", - "typescript": "~5.6.2" + "@nestjs/cli": "12.0.11", + "@nestjs/testing": "12.3.2", + "@stylistic/eslint-plugin": "4.4.1", + "@swc/cli": "0.8.2", + "@swc/core": "1.16.21", + "@types/better-sqlite3": "7.6.13", + "@types/express": "5.0.6", + "@types/node": "24.19.2", + "@types/supertest": "7.2.1", + "oxfmt": "0.75.0", + "oxlint": "1.90.0", + "supertest": "7.3.1", + "typescript": "6.0.3", + "unplugin-swc": "2.0.0", + "vitest": "3.2.7" + }, + "engines": { + "node": ">=26.13.0" } } diff --git a/api/scripts/install-better-sqlite3.cjs b/api/scripts/install-better-sqlite3.cjs index d3a86d2..672c8d1 100755 --- a/api/scripts/install-better-sqlite3.cjs +++ b/api/scripts/install-better-sqlite3.cjs @@ -6,80 +6,154 @@ * Falls back to npm rebuild if download fails */ -const https = require('https'); -const fs = require('fs'); -const path = require('path'); -const { execSync } = require('child_process'); +const https = require('https') +const fs = require('fs') +const path = require('path') +const { execSync } = require('child_process') // Extract better-sqlite3 version from package.json -const packageJson = require('../package.json'); -const BETTER_SQLITE3_VERSION = packageJson.dependencies['better-sqlite3'].replace(/[\^~]/, ''); -const NODE_ABI_VERSION = process.versions.modules; -const PLATFORM = process.platform; -const ARCH = process.arch; - -const BINARY_NAME = 'better_sqlite3.node'; -const BINARY_URL = `https://github.com/WiseLibs/better-sqlite3/releases/download/v${BETTER_SQLITE3_VERSION}/better-sqlite3-v${BETTER_SQLITE3_VERSION}-node-v${NODE_ABI_VERSION}-${PLATFORM}-${ARCH}.tar.gz`; - -const BETTER_SQLITE3_PATH = path.join( - __dirname, - '..', - 'node_modules', - 'better-sqlite3' -); - -const BUILD_DIR = path.join(BETTER_SQLITE3_PATH, 'build', 'Release'); -const BINARY_PATH = path.join(BUILD_DIR, BINARY_NAME); - -// Check if binary already exists +const packageJson = require('../package.json') +const BETTER_SQLITE3_VERSION = packageJson.dependencies['better-sqlite3'].replace(/[\^~]/, '') +const NODE_ABI_VERSION = process.versions.modules +const PLATFORM = process.platform +const ARCH = process.arch + +const BINARY_NAME = 'better_sqlite3.node' +const BINARY_URL = `https://github.com/WiseLibs/better-sqlite3/releases/download/v${BETTER_SQLITE3_VERSION}/better-sqlite3-v${BETTER_SQLITE3_VERSION}-node-v${NODE_ABI_VERSION}-${PLATFORM}-${ARCH}.tar.gz` + +const BETTER_SQLITE3_PATH = path.join(__dirname, '..', 'node_modules', 'better-sqlite3') + +const BUILD_DIR = path.join(BETTER_SQLITE3_PATH, 'build', 'Release') +const BINARY_PATH = path.join(BUILD_DIR, BINARY_NAME) +const STAMP_PATH = path.join(BUILD_DIR, '.better-sqlite3-stamp.json') + +function expectedStamp() { + return { + version: BETTER_SQLITE3_VERSION, + nodeAbi: String(NODE_ABI_VERSION), + platform: PLATFORM, + arch: ARCH, + } +} + +function stampMatches() { + if (!fs.existsSync(STAMP_PATH) || !fs.existsSync(BINARY_PATH)) { + return false + } + try { + const stamp = JSON.parse(fs.readFileSync(STAMP_PATH, 'utf8')) + const want = expectedStamp() + + return ( + stamp.version === want.version && + stamp.nodeAbi === want.nodeAbi && + stamp.platform === want.platform && + stamp.arch === want.arch + ) + } catch { + return false + } +} + +function writeStamp() { + fs.mkdirSync(BUILD_DIR, { recursive: true }) + fs.writeFileSync(STAMP_PATH, JSON.stringify(expectedStamp())) +} + +function removeInstalledBinary() { + for (const p of [BINARY_PATH, STAMP_PATH]) { + if (fs.existsSync(p)) { + fs.rmSync(p, { force: true }) + } + } +} + +/** Darwin needs Mach-O; a Linux ELF left in node_modules (e.g. after Docker) fails dlopen. */ +function binaryLooksNative() { + if (PLATFORM !== 'darwin' || !fs.existsSync(BINARY_PATH)) { + return true + } + try { + const kind = execSync(`file -b ${JSON.stringify(BINARY_PATH)}`, { encoding: 'utf8' }) + + return kind.includes('Mach-O') + } catch { + return false + } +} + +function canOpenDatabase() { + if (!fs.existsSync(BINARY_PATH)) { + return false + } + try { + const Database = require(BETTER_SQLITE3_PATH) + const db = new Database(':memory:') + + db.close() + + return true + } catch { + return false + } +} + +if (stampMatches() && binaryLooksNative() && canOpenDatabase()) { + console.log(`✓ better-sqlite3 binary OK for Node ${process.version} (${PLATFORM}-${ARCH})`) + process.exit(0) +} + if (fs.existsSync(BINARY_PATH)) { - console.log(`✓ better-sqlite3 binary already exists at ${BINARY_PATH}`); - process.exit(0); + console.log('Replacing better-sqlite3 binary (wrong platform, Node ABI, or corrupt)') + removeInstalledBinary() } -console.log(`Downloading better-sqlite3 pre-built binary...`); -console.log(` Version: ${BETTER_SQLITE3_VERSION}`); -console.log(` Platform: ${PLATFORM}-${ARCH}`); -console.log(` Node ABI: v${NODE_ABI_VERSION}`); -console.log(` URL: ${BINARY_URL}`); +console.log(`Downloading better-sqlite3 pre-built binary...`) +console.log(` Version: ${BETTER_SQLITE3_VERSION}`) +console.log(` Platform: ${PLATFORM}-${ARCH}`) +console.log(` Node ABI: v${NODE_ABI_VERSION}`) +console.log(` URL: ${BINARY_URL}`) // Ensure build directory exists -fs.mkdirSync(BUILD_DIR, { recursive: true }); +fs.mkdirSync(BUILD_DIR, { recursive: true }) -const tempTarball = path.join('/tmp', `better-sqlite3-${Date.now()}.tar.gz`); -const file = fs.createWriteStream(tempTarball); +const tempTarball = path.join('/tmp', `better-sqlite3-${Date.now()}.tar.gz`) +const file = fs.createWriteStream(tempTarball) /** * Extract the tarball and move binary to correct location */ function extractBinary() { try { - console.log(`✓ Downloaded to ${tempTarball}`); - console.log(`Extracting...`); - + console.log(`✓ Downloaded to ${tempTarball}`) + console.log(`Extracting...`) + // Extract tarball - execSync(`tar -xzf ${tempTarball} -C ${BUILD_DIR}`, { stdio: 'inherit' }); - + execSync(`tar -xzf ${tempTarball} -C ${BUILD_DIR}`, { stdio: 'inherit' }) + // Handle nested build directory structure - const extractedBuildDir = path.join(BUILD_DIR, 'build', 'Release', BINARY_NAME); + const extractedBuildDir = path.join(BUILD_DIR, 'build', 'Release', BINARY_NAME) + if (fs.existsSync(extractedBuildDir)) { - fs.renameSync(extractedBuildDir, BINARY_PATH); - fs.rmSync(path.join(BUILD_DIR, 'build'), { recursive: true }); + fs.renameSync(extractedBuildDir, BINARY_PATH) + fs.rmSync(path.join(BUILD_DIR, 'build'), { recursive: true }) } - + // Cleanup temp file - fs.unlinkSync(tempTarball); - - if (fs.existsSync(BINARY_PATH)) { - console.log(`✓ better-sqlite3 binary installed at ${BINARY_PATH}`); - process.exit(0); + fs.unlinkSync(tempTarball) + + if (fs.existsSync(BINARY_PATH) && canOpenDatabase()) { + writeStamp() + console.log(`✓ better-sqlite3 binary installed at ${BINARY_PATH}`) + process.exit(0) } else { - console.error(`✗ Binary not found after extraction`); - fallbackToBuild(); + console.error(`✗ Binary not loadable after extraction`) + removeInstalledBinary() + fallbackToBuild() } } catch (err) { - console.error(`✗ Extraction failed:`, err.message); - fallbackToBuild(); + console.error(`✗ Extraction failed:`, err.message) + fallbackToBuild() } } @@ -87,17 +161,21 @@ function extractBinary() { * Fallback to building from source */ function fallbackToBuild() { - console.log(`\nFalling back to build from source...`); + console.log(`\nFalling back to build from source...`) try { - execSync('npm rebuild better-sqlite3', { + execSync('npm rebuild better-sqlite3', { stdio: 'inherit', - cwd: path.join(__dirname, '..') - }); - console.log(`✓ better-sqlite3 built from source successfully`); - process.exit(0); + cwd: path.join(__dirname, '..'), + }) + if (!canOpenDatabase()) { + throw new Error('rebuild succeeded but module still fails to load') + } + writeStamp() + console.log(`✓ better-sqlite3 built from source successfully`) + process.exit(0) } catch (err) { - console.error(`✗ Build from source failed:`, err.message); - process.exit(1); + console.error(`✗ Build from source failed:`, err.message) + process.exit(1) } } @@ -107,21 +185,21 @@ function fallbackToBuild() { function handleResponse(response) { if (response.statusCode === 302 || response.statusCode === 301) { // Follow redirect - https.get(response.headers.location, handleResponse).on('error', handleError); + https.get(response.headers.location, handleResponse).on('error', handleError) } else if (response.statusCode === 200) { - response.pipe(file); + response.pipe(file) file.on('finish', () => { - file.close(extractBinary); - }); + file.close(extractBinary) + }) } else if (response.statusCode === 404) { - console.error(`✗ Pre-built binary not available for this platform`); - console.error(` HTTP ${response.statusCode}: ${BINARY_URL}`); - fs.unlinkSync(tempTarball); - fallbackToBuild(); + console.error(`✗ Pre-built binary not available for this platform`) + console.error(` HTTP ${response.statusCode}: ${BINARY_URL}`) + fs.unlinkSync(tempTarball) + fallbackToBuild() } else { - console.error(`✗ Failed to download: HTTP ${response.statusCode}`); - fs.unlinkSync(tempTarball); - fallbackToBuild(); + console.error(`✗ Failed to download: HTTP ${response.statusCode}`) + fs.unlinkSync(tempTarball) + fallbackToBuild() } } @@ -130,11 +208,11 @@ function handleResponse(response) { */ function handleError(err) { if (fs.existsSync(tempTarball)) { - fs.unlinkSync(tempTarball); + fs.unlinkSync(tempTarball) } - console.error(`✗ Download failed:`, err.message); - fallbackToBuild(); + console.error(`✗ Download failed:`, err.message) + fallbackToBuild() } // Start download -https.get(BINARY_URL, handleResponse).on('error', handleError); +https.get(BINARY_URL, handleResponse).on('error', handleError) diff --git a/api/src/access/access.guard.ts b/api/src/access/access.guard.ts new file mode 100644 index 0000000..050152d --- /dev/null +++ b/api/src/access/access.guard.ts @@ -0,0 +1,81 @@ +import { + CanActivate, + ExecutionContext, + Inject, + Injectable, + ForbiddenException, + Optional, + UnauthorizedException, + createParamDecorator, +} from '@nestjs/common' +import type { Request } from 'express' +import { UsersService } from '../users/users.service' +import { ACCESS_SETTINGS, ACCESS_VERIFIER, AccessSettings, AccessVerifier, Actor } from './access.settings' + +export type ActorRequest = Request & { actor?: Actor } + +/** Callback for features that want to record refused requests (the bank audit log does). */ +export const ACCESS_REFUSED = Symbol('ACCESS_REFUSED') +type AccessRefusedHandler = (path: string, reason: string) => void + +/** Only signed-in users get through. */ +@Injectable() +export class AccessGuard implements CanActivate { + constructor( + @Inject(ACCESS_SETTINGS) private readonly settings: AccessSettings, + @Optional() + @Inject(ACCESS_VERIFIER) + private readonly verify: AccessVerifier | null, + private readonly users: UsersService, + @Optional() + @Inject(ACCESS_REFUSED) + private readonly onRefused?: AccessRefusedHandler, + ) {} + + async canActivate(context: ExecutionContext): Promise { + const req = context.switchToHttp().getRequest() + + if (this.settings.mode === 'dev') { + // No Cloudflare in front: the developer is dev@localhost. X-Dev-User lets you try a second person locally. + const email = (!this.settings.isProd && req.header('x-dev-user')) || 'dev@localhost' + + return this.identify(req, email) + } + const token = req.header('cf-access-jwt-assertion') + + if (!token || !this.verify) { + return this.refuse(req, 'no token') + } + let email: string + + try { + email = (await this.verify(token)).email + } catch { + return this.refuse(req, 'invalid token') + } + + return this.identify(req, email) + } + + private identify(req: ActorRequest, email: string): boolean { + const user = this.users.ensure(email) + + if (!user) { + this.onRefused?.(req.path, 'user not allowed') + throw new ForbiddenException({ error: 'Forbidden' }) + } + req.actor = { email: user.email, userId: user.id } + + return true + } + + private refuse(req: Request, reason: string): never { + this.onRefused?.(req.path, reason) + throw new UnauthorizedException({ error: 'Unauthorized' }) + } +} + +/** The signed-in user (set by AccessGuard). */ +export const CurrentActor = createParamDecorator( + (_: unknown, ctx: ExecutionContext): Actor => ctx.switchToHttp().getRequest().actor!, +) diff --git a/api/src/access/access.module.ts b/api/src/access/access.module.ts new file mode 100644 index 0000000..3d3ba50 --- /dev/null +++ b/api/src/access/access.module.ts @@ -0,0 +1,29 @@ +import { DynamicModule, Global, Module } from '@nestjs/common' +import { AccessGuard } from './access.guard' +import { CsrfGuard } from './csrf.guard' +import { ACCESS_SETTINGS, ACCESS_VERIFIER, loadAccessSettings } from './access.settings' +import { cloudflareVerifier } from './cloudflare-verifier' + +@Global() +@Module({}) +export class AccessModule { + /** Reads the sign-in settings from the environment once, at start-up. */ + static forRoot(env: NodeJS.ProcessEnv = process.env): DynamicModule { + const settings = loadAccessSettings(env) + + return { + module: AccessModule, + providers: [ + { provide: ACCESS_SETTINGS, useValue: settings }, + { + provide: ACCESS_VERIFIER, + useValue: + settings.mode === 'cloudflare' ? cloudflareVerifier(settings.teamDomain!, settings.audience!) : null, + }, + AccessGuard, + CsrfGuard, + ], + exports: [ACCESS_SETTINGS, ACCESS_VERIFIER, AccessGuard, CsrfGuard], + } + } +} diff --git a/api/src/access/access.settings.ts b/api/src/access/access.settings.ts new file mode 100644 index 0000000..e6b07bc --- /dev/null +++ b/api/src/access/access.settings.ts @@ -0,0 +1,60 @@ +export interface AccessSettings { + isProd: boolean + mode: 'cloudflare' | 'dev' + teamDomain?: string + audience?: string + /** Everyone who may use this FinKit. They are all equal, and each sees only their own data. */ + allowedEmails: string[] + /** Who inherits data from before user accounts existed: the first address listed, never first to arrive. */ + legacyOwner?: string + allowedOrigin?: string +} + +export const ACCESS_SETTINGS = Symbol('ACCESS_SETTINGS') +export const ACCESS_VERIFIER = Symbol('ACCESS_VERIFIER') +export type AccessVerifier = (token: string) => Promise<{ email: string }> + +export interface Actor { + email: string + /** Whose data this request may touch. Every stored row carries it. */ + userId: string +} + +/** + * In production, requests must carry a Cloudflare Access token that the server verifies itself, and + * the email in it must be on the allowed list. Locally there is no Access: the developer is + * dev@localhost, and X-Dev-User lets you try other people. In production, missing Access settings + * stop the server from starting. + */ +export function loadAccessSettings(env: NodeJS.ProcessEnv = process.env): AccessSettings { + const teamDomain = env.CF_ACCESS_TEAM_DOMAIN + const audience = env.CF_ACCESS_AUD + const isProd = env.NODE_ENV === 'production' + + // Fail closed: in production a missing Cloudflare setup must never fall back to the shared dev identity + if (isProd && (!teamDomain || !audience)) { + throw new Error('CF_ACCESS_TEAM_DOMAIN and CF_ACCESS_AUD are required in production') + } + // Same for the web app's address: without it Origin can't be enforced and CORS would be open to everyone + if (isProd && !env.ALLOWED_ORIGIN) { + throw new Error('ALLOWED_ORIGIN is required in production (the web app origin, e.g. https://finkit.example.com)') + } + const mode = teamDomain && audience ? 'cloudflare' : 'dev' + // BANKING_ADMIN_EMAILS and ADMIN_EMAILS are the old names for this list + const listed = (env.ALLOWED_USER_EMAILS || env.BANKING_ADMIN_EMAILS || env.ADMIN_EMAILS || '') + .split(',') + .map(s => s.trim().toLowerCase()) + .filter(Boolean) + const allowedEmails = mode === 'dev' ? [...new Set(['dev@localhost', ...listed])] : listed + + return { + isProd, + mode, + teamDomain, + audience, + allowedEmails, + // With nobody listed (and not in development) nobody gets in + legacyOwner: mode === 'dev' ? 'dev@localhost' : listed[0], + allowedOrigin: env.ALLOWED_ORIGIN, + } +} diff --git a/api/src/access/cloudflare-verifier.ts b/api/src/access/cloudflare-verifier.ts new file mode 100644 index 0000000..5eb360b --- /dev/null +++ b/api/src/access/cloudflare-verifier.ts @@ -0,0 +1,19 @@ +import { createRemoteJWKSet, jwtVerify } from 'jose' +import { AccessVerifier } from './access.settings' + +/** Verifies the token Cloudflare Access attaches to every request it lets through. */ +export function cloudflareVerifier(teamDomain: string, audience: string): AccessVerifier { + const issuer = `https://${teamDomain}.cloudflareaccess.com` + const jwks = createRemoteJWKSet(new URL(`${issuer}/cdn-cgi/access/certs`)) + + return async token => { + const { payload } = await jwtVerify(token, jwks, { issuer, audience }) + const email = typeof payload.email === 'string' ? payload.email.toLowerCase() : '' + + if (!email) { + throw new Error('Token has no email') + } + + return { email } + } +} diff --git a/api/src/access/csrf.guard.ts b/api/src/access/csrf.guard.ts new file mode 100644 index 0000000..3db0eb3 --- /dev/null +++ b/api/src/access/csrf.guard.ts @@ -0,0 +1,31 @@ +import { CanActivate, ExecutionContext, ForbiddenException, Inject, Injectable } from '@nestjs/common' +import type { Request } from 'express' +import { ACCESS_SETTINGS, AccessSettings } from './access.settings' + +/** + * Changing requests need a header a plain cross-site form or image tag can't send, and must + * come from the allowed origin when one is configured. A missing Origin is refused too; Referer is + * deliberately not used as a fallback. With no allowed origin configured (local development), Origin is not checked. + */ +@Injectable() +export class CsrfGuard implements CanActivate { + constructor(@Inject(ACCESS_SETTINGS) private readonly settings: AccessSettings) {} + + canActivate(context: ExecutionContext): boolean { + const req = context.switchToHttp().getRequest() + + if (req.method === 'GET' || req.method === 'HEAD' || req.method === 'OPTIONS') { + return true + } + if (req.header('x-finkit') !== '1') { + throw new ForbiddenException({ error: 'Forbidden' }) + } + const origin = req.header('origin') + + if (this.settings.allowedOrigin && origin !== this.settings.allowedOrigin) { + throw new ForbiddenException({ error: 'Forbidden' }) + } + + return true + } +} diff --git a/api/src/app.module.ts b/api/src/app.module.ts new file mode 100644 index 0000000..89d22ea --- /dev/null +++ b/api/src/app.module.ts @@ -0,0 +1,28 @@ +import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common' +import { AccessModule } from './access/access.module' +import { BankConnectionsModule } from './bank-connections/bank-connections.module' +import { HealthModule } from './health/health.module' +import { MerchantRulesModule } from './merchant-rules/merchant-rules.module' +import { SettingsModule } from './settings/settings.module' +import { SheetExportModule } from './sheet-export/sheet-export.module' +import { UsersModule } from './users/users.module' +import { DatabaseModule } from './shared/database/database.module' +import { RequestLoggerMiddleware } from './shared/http/request-logger.middleware' + +@Module({ + imports: [ + DatabaseModule.forRoot(), + UsersModule, + AccessModule.forRoot(), + HealthModule, + MerchantRulesModule, + SettingsModule, + SheetExportModule, + BankConnectionsModule.forRoot(), + ], +}) +export class AppModule implements NestModule { + configure(consumer: MiddlewareConsumer) { + consumer.apply(RequestLoggerMiddleware).forRoutes('{*path}') + } +} diff --git a/api/src/app.setup.ts b/api/src/app.setup.ts new file mode 100644 index 0000000..de7a033 --- /dev/null +++ b/api/src/app.setup.ts @@ -0,0 +1,21 @@ +import { INestApplication } from '@nestjs/common' +import { NestExpressApplication } from '@nestjs/platform-express' +import helmet from 'helmet' +import { HttpErrorFilter } from './shared/http/http-error.filter' + +/** HTTP behaviour shared by the real server and the tests. */ +export function configureApp(app: INestApplication, env: NodeJS.ProcessEnv = process.env) { + const express = app as NestExpressApplication + + express.useBodyParser('json', { limit: '5mb' }) // large batch learn/export payloads + app.use(helmet()) + app.enableCors({ + // Production must set ALLOWED_ORIGIN (checked at start-up); locally the dev servers are allowed. + origin: env.NODE_ENV === 'production' ? env.ALLOWED_ORIGIN : ['http://localhost:5173', 'http://localhost:3000'], + methods: ['GET', 'POST', 'DELETE', 'OPTIONS'], + allowedHeaders: ['Content-Type', 'X-FinKit'], + }) + app.useGlobalFilters(new HttpErrorFilter()) + + return app +} diff --git a/api/src/app.test.ts b/api/src/app.test.ts new file mode 100644 index 0000000..19960e6 --- /dev/null +++ b/api/src/app.test.ts @@ -0,0 +1,342 @@ +import { afterEach, describe, expect, it } from 'vitest' +import request from 'supertest' +import { createTestApp } from './testing/test-app' + +type TestApp = Awaited> +const open: TestApp[] = [] + +async function make( + env?: NodeJS.ProcessEnv, + verify?: Parameters[0] extends infer O ? (O extends { verify?: infer V } ? V : never) : never, +) { + const t = await createTestApp({ env, verify }) + + open.push(t) + + return t +} +afterEach(async () => { + while (open.length) { + await open.pop()!.close() + } +}) +const json = { 'Content-Type': 'application/json', 'X-FinKit': '1' } + +describe('health', () => { + it('answers on /health and /api/health', async () => { + const t = await make() + + for (const path of ['/health', '/api/health']) { + expect((await request(t.server).get(path).expect(200)).body.status).toBe('ok') + } + }) +}) + +describe('merchant rules (/api/learn)', () => { + it('learns, lists, updates, and forgets a rule', async () => { + const t = await make() + + expect( + ( + await request(t.server) + .post('/api/learn/mapping') + .set(json) + .send({ merchant: 'DeepSeek', category: 'Subscriptions' }) + .expect(200) + ).body, + ).toEqual({ + success: true, + message: 'Learned: "DeepSeek" → "Subscriptions"', + }) + await request(t.server) + .post('/api/learn/mapping') + .set(json) + .send({ merchant: 'DeepSeek', category: 'Software' }) + .expect(200) + const list = (await request(t.server).get('/api/learn/mappings').expect(200)).body + + expect(list).toMatchObject({ + success: true, + data: [ + { + merchant: 'deepseek', + merchantDisplay: 'DeepSeek', + category: 'Software', + confidence: 2, + }, + ], + }) + expect((await request(t.server).delete('/api/learn/mapping/DeepSeek').set(json).expect(200)).body.message).toBe( + 'Deleted mapping for "DeepSeek"', + ) + expect((await request(t.server).get('/api/learn/mappings')).body.data).toEqual([]) + }) + it('saves a batch and reports stats', async () => { + const t = await make() + + await request(t.server) + .post('/api/learn/mappings/batch') + .set(json) + .send({ + mappings: [ + { merchant: 'REWE', category: 'Groceries' }, + { merchant: 'Lidl', category: 'Groceries' }, + ], + }) + .expect(200) + const stats = (await request(t.server).get('/api/learn/stats').expect(200)).body.data + + expect(stats.total).toBe(2) + expect(stats.topCategories[0]).toEqual({ category: 'Groceries', count: 2 }) + }) + it('decodes merchant names with special characters once', async () => { + const t = await make() + + await request(t.server).post('/api/learn/mapping').set(json).send({ merchant: 'Café 100%', category: 'Eating Out' }) + await request(t.server) + .delete(`/api/learn/mapping/${encodeURIComponent('Café 100%')}`) + .set(json) + .expect(200) + expect((await request(t.server).get('/api/learn/mappings')).body.data).toEqual([]) + }) + it('rejects invalid bodies in the established shape', async () => { + const t = await make() + const r = await request(t.server).post('/api/learn/mapping').set(json).send({ merchant: '' }).expect(400) + + expect(r.body.error).toBe('Invalid request') + expect(Array.isArray(r.body.details)).toBe(true) + await request(t.server).post('/api/learn/mappings/batch').set(json).send({ mappings: 'x' }).expect(400) + }) +}) + +describe('settings (/api/settings)', () => { + it('serves defaults, saves, and resets the sheet configuration', async () => { + const t = await make() + const defaults = (await request(t.server).get('/api/settings/sheet').expect(200)).body.data + + expect(defaults).toMatchObject({ + sheetId: '', + tabName: '2026', + monthFormat: 'short', + constantColumns: ['S', 'U', 'X'], + }) + expect(defaults.categoryMappings).toHaveLength(17) + await request(t.server) + .post('/api/settings/sheet') + .set(json) + .send({ + sheetId: 'abc', + tabName: '2027', + layout: [{ column: 'A', name: 'Month', kind: 'month' }], + headerRow: 2, + }) + .expect(200) + const saved = (await request(t.server).get('/api/settings/sheet')).body.data + + expect(saved).toMatchObject({ + sheetId: 'abc', + tabName: '2027', + headerRow: 2, + layout: [{ column: 'A', name: 'Month', kind: 'month' }], + }) + await request(t.server).delete('/api/settings/sheet').set(json).expect(200) + expect((await request(t.server).get('/api/settings/sheet')).body.data.sheetId).toBe('') + }) + it('rejects an invalid column kind', async () => { + const t = await make() + const r = await request(t.server) + .post('/api/settings/sheet') + .set(json) + .send({ layout: [{ column: 'A', name: 'x', kind: 'nope' }] }) + .expect(400) + + expect(r.body.success).toBe(false) + expect(Array.isArray(r.body.error)).toBe(true) + }) + it('keeps generic key-value settings, with the fixed routes taking priority', async () => { + const t = await make() + + await request(t.server).get('/api/settings/theme').expect(404) + await request(t.server).post('/api/settings/theme').set(json).send({ value: 'dark' }).expect(200) + expect((await request(t.server).get('/api/settings/theme')).body).toEqual({ + success: true, + data: 'dark', + }) + const all = (await request(t.server).get('/api/settings')).body.data + + expect(all.theme).toBe('dark') + expect(all.sheetConfig.tabName).toBe('2026') + }) +}) + +describe('sheet export (/api/export)', () => { + it('validates requests before touching Google', async () => { + const t = await make() + + expect((await request(t.server).post('/api/export/sheets').set(json).send({}).expect(400)).body.error).toBe( + 'Invalid request body', + ) + expect((await request(t.server).post('/api/export/sheets/layout').set(json).send({}).expect(400)).body).toEqual({ + error: 'Missing sheetId or tabName', + }) + expect((await request(t.server).post('/api/export/sheets/metadata').set(json).send({}).expect(400)).body).toEqual({ + error: 'Missing sheetId', + }) + }) + it('hides internal error details', async () => { + delete process.env.GOOGLE_SERVICE_ACCOUNT_KEY_PATH + const t = await make() + const r = await request(t.server).post('/api/export/sheets/metadata').set(json).send({ sheetId: 'x' }).expect(500) + + expect(r.body).toEqual({ error: 'Failed to fetch sheet metadata' }) + expect(JSON.stringify(r.body)).not.toMatch(/GOOGLE_SERVICE_ACCOUNT_KEY_PATH/) + }) +}) + +describe('sheet export input checks', () => { + const body = { + sheetId: 'abc123', + tabName: '2026', + month: 'July 2026', + monthFormat: 'long', + categoryValues: [{ column: 'B', value: 1 }], + income: 1, + } + const post = (t: Awaited>, path: string, b: object) => + request(t.server).post(`/api/export/${path}`).set(json).send(b) + + it('rejects malformed columns and tab names before reaching Google', async () => { + const t = await make() + + for (const bad of [ + { categoryValues: [{ column: "B1'!A1", value: 1 }] }, + { categoryValues: [{ column: 'b', value: 1 }] }, + { incomeColumn: 'T2' }, + { constantColumns: ['S!'] }, + { tabName: 'Sheet1!A1:Z9:' }, + { tabName: 'a\nb' }, + { tabName: 'x'.repeat(101) }, + { sheetId: "../x'" }, + ]) { + const r = await post(t, 'sheets', { ...body, ...bad }).expect(400) + + expect(JSON.stringify(r.body)).not.toMatch(/googleapis|GOOGLE_SERVICE/) + } + await post(t, 'sheets/layout', { sheetId: 'abc', tabName: 'a*b' }).expect(400) + await post(t, 'sheets/metadata', { sheetId: 'a b' }).expect(400) + }) + it('lets well-formed requests through to Google (and then fails generically with no key)', async () => { + delete process.env.GOOGLE_SERVICE_ACCOUNT_KEY_PATH + const t = await make() + + expect((await post(t, 'sheets', { ...body, tabName: "Bob's budget" }).expect(500)).body).toEqual({ + error: 'Failed to export to Google Sheets', + }) + }) +}) + +describe('production access', () => { + it('refuses to start without Cloudflare Access', async () => { + await expect(make({ NODE_ENV: 'production', ALLOWED_ORIGIN: 'https://finkit.test' })).rejects.toThrow(/CF_ACCESS/) + }) + it('refuses to start without ALLOWED_ORIGIN', async () => { + await expect( + make({ NODE_ENV: 'production', CF_ACCESS_TEAM_DOMAIN: 'x.cloudflareaccess.com', CF_ACCESS_AUD: 'aud' }), + ).rejects.toThrow(/ALLOWED_ORIGIN/) + }) + it('keeps users apart when Access is configured', async () => { + const t = await make( + { + NODE_ENV: 'production', + ALLOWED_ORIGIN: 'https://finkit.test', + CF_ACCESS_TEAM_DOMAIN: 'x.cloudflareaccess.com', + CF_ACCESS_AUD: 'aud', + ALLOWED_USER_EMAILS: 'a@x.com,b@x.com', + }, + async token => ({ email: token }), + ) + const as = (who: string) => ({ ...json, 'Cf-Access-Jwt-Assertion': who, Origin: 'https://finkit.test' }) + + await request(t.server) + .post('/api/learn/mapping') + .set(as('a@x.com')) + .send({ merchant: 'REWE', category: 'Groceries' }) + .expect(200) + expect((await request(t.server).get('/api/learn/mappings').set(as('a@x.com'))).body.data).toHaveLength(1) + expect((await request(t.server).get('/api/learn/mappings').set(as('b@x.com'))).body.data).toHaveLength(0) + await request(t.server).get('/api/learn/mappings').expect(401) + }) +}) + +describe('CSRF origin', () => { + it('requires the allowed Origin on changing requests', async () => { + const t = await make({ ALLOWED_ORIGIN: 'http://localhost:5173' }) + const body = { merchant: 'REWE', category: 'Groceries' } + + await request(t.server).post('/api/learn/mapping').set(json).send(body).expect(403) + await request(t.server) + .post('/api/learn/mapping') + .set(json) + .set('Origin', 'http://evil.test') + .send(body) + .expect(403) + await request(t.server) + .post('/api/learn/mapping') + .set(json) + .set('Origin', 'http://localhost:5173') + .send(body) + .expect(200) + }) +}) + +describe('cross-cutting behaviour', () => { + it('answers unknown routes with the established 404 body', async () => { + const t = await make() + + expect((await request(t.server).get('/api/nope').expect(404)).body).toEqual({ error: 'Not Found' }) + }) + it('answers malformed JSON with a clean 400', async () => { + const t = await make() + const r = await request(t.server).post('/api/learn/mapping').set(json).send('{not json').expect(400) + + expect(r.body).toEqual({ error: 'Bad Request' }) + }) + it('sets security headers', async () => { + const t = await make() + const h = (await request(t.server).get('/health')).headers + + expect(h['x-content-type-options']).toBe('nosniff') + expect(h['x-frame-options']).toBeDefined() + expect(h['x-powered-by']).toBeUndefined() + }) + it('allows the custom header in CORS preflights', async () => { + const t = await make() + const r = await request(t.server) + .options('/api/banking/connect') + .set('Origin', 'http://localhost:5173') + .set('Access-Control-Request-Method', 'POST') + .set('Access-Control-Request-Headers', 'x-finkit,content-type') + + expect(String(r.headers['access-control-allow-headers']).toLowerCase()).toContain('x-finkit') + }) + it('accepts large bodies (batch learn mappings)', async () => { + const t = await make() + const big = { + mappings: Array.from({ length: 15000 }, (_, i) => ({ + merchant: `Merchant number ${i}`, + category: 'Other', + })), + } + + expect(JSON.stringify(big).length).toBeGreaterThan(500_000) // well above the default 100 kB limit + await request(t.server).post('/api/learn/mappings/batch').set(json).send(big).expect(200) + }) + it('never leaks internals on unexpected errors', async () => { + const t = await make() + + t.db.exec('DROP TABLE merchant_categories') // makes the next query throw + const r = await request(t.server).get('/api/learn/mappings').expect(500) + + expect(r.body).toEqual({ error: 'Internal Server Error' }) + }) +}) diff --git a/api/src/bank-connections/admin/bank-admin.controller.ts b/api/src/bank-connections/admin/bank-admin.controller.ts new file mode 100644 index 0000000..0229130 --- /dev/null +++ b/api/src/bank-connections/admin/bank-admin.controller.ts @@ -0,0 +1,26 @@ +import { Controller, Get, Query, UseGuards, UseInterceptors } from '@nestjs/common' +import { AccessGuard, CurrentActor } from '../../access/access.guard' +import { Actor } from '../../access/access.settings' +import { CsrfGuard } from '../../access/csrf.guard' +import { NoStoreInterceptor } from '../../shared/http/no-store.interceptor' +import { BankConnectionsService } from '../connections/bank-connections.service' + +/** Back office: is it set up correctly, and what ran and what you did. Everyone sees only their own activity. */ +@Controller('api/banking/admin') +@UseGuards(AccessGuard, CsrfGuard) +@UseInterceptors(NoStoreInterceptor) +export class BankAdminController { + constructor(private readonly connections: BankConnectionsService) {} + + @Get('overview') overview(@CurrentActor() actor: Actor) { + return this.connections.overview(actor) + } + + @Get('sync-log') syncLog(@CurrentActor() actor: Actor, @Query('limit') limit?: string) { + return this.connections.syncLog(actor, Number(limit)) + } + + @Get('audit') audit(@CurrentActor() actor: Actor, @Query('limit') limit?: string) { + return this.connections.auditLog(actor, Number(limit)) + } +} diff --git a/api/src/bank-connections/bank-connections.module.ts b/api/src/bank-connections/bank-connections.module.ts new file mode 100644 index 0000000..618ad70 --- /dev/null +++ b/api/src/bank-connections/bank-connections.module.ts @@ -0,0 +1,98 @@ +import { DynamicModule, Logger, Module } from '@nestjs/common' +import { ACCESS_REFUSED } from '../access/access.guard' +import { loadAccessSettings } from '../access/access.settings' +import { loadBankingConfig } from './config/banking.config' +import { BankAdminController } from './admin/bank-admin.controller' +import { BankCallbackController } from './connections/bank-callback.controller' +import { BankConnectionsController } from './connections/bank-connections.controller' +import { BankConnectionsService } from './connections/bank-connections.service' +import { BankAutoSync } from './sync/bank-auto-sync' +import { BankSyncController } from './sync/bank-sync.controller' +import { BankSyncService } from './sync/bank-sync.service' +import { StoredTransactionsController } from './transactions/stored-transactions.controller' +import { StoredTransactionsService } from './transactions/stored-transactions.service' +import { BankConnectionsStore } from './connections/bank-connections.store' +import { BankTransactionsStore } from './transactions/bank-transactions.store' +import { BANKING_CONFIG, BANKING_STATE, CLOCK } from './config/bank-connections.tokens' +import { BANK_PROVIDER } from './providers/bank-provider' +import { BankingUnavailableController } from './banking-unavailable.controller' +import { EnableBankingProvider } from './providers/enable-banking.provider' +import { TestBankProvider } from './providers/test-bank.provider' +import { TestBankController } from './providers/test-bank.controller' + +@Module({}) +export class BankConnectionsModule { + /** + * Decides once, at start-up, what to expose. Banking fails closed: when it is off, or set up + * incorrectly (for example in production without sign-in), only a "disabled" status is served. + */ + static forRoot(env: NodeJS.ProcessEnv = process.env): DynamicModule { + const logger = new Logger('BankConnections') + const access = loadAccessSettings(env) + const { config, errors } = loadBankingConfig(env) + const problems = [ + ...errors, + ...(config && access.isProd && access.mode !== 'cloudflare' + ? ['CF_ACCESS_TEAM_DOMAIN and CF_ACCESS_AUD are required in production'] + : []), + ] + + if (!config || problems.length) { + if (problems.length) { + logger.error(`Banking is disabled: ${problems.join('; ')}`) + } + + return { + module: BankConnectionsModule, + controllers: [BankingUnavailableController], + providers: [ + { + provide: BANKING_STATE, + useValue: problems.length ? 'misconfigured' : 'off', + }, + ], + } + } + + logger.log(`Banking enabled (${config.provider}, access: ${access.mode})`) + const useTestBank = config.provider === 'mock' + + return { + module: BankConnectionsModule, + controllers: [ + BankConnectionsController, + BankSyncController, + StoredTransactionsController, + BankCallbackController, + BankAdminController, + ...(useTestBank && !config.isProd ? [TestBankController] : []), + ], + providers: [ + { provide: BANKING_CONFIG, useValue: config }, + { provide: CLOCK, useValue: { now: () => new Date() } }, + { + provide: BANK_PROVIDER, + useValue: useTestBank + ? new TestBankProvider() + : new EnableBankingProvider({ + appId: config.ebAppId!, + privateKey: config.ebPrivateKey!, + }), + }, + BankConnectionsStore, + BankTransactionsStore, + BankConnectionsService, + BankSyncService, + StoredTransactionsService, + BankAutoSync, + // Refused sign-ins are recorded in the bank audit log + { + provide: ACCESS_REFUSED, + inject: [BankConnectionsStore], + useFactory: (store: BankConnectionsStore) => (path: string, reason: string) => + store.audit('unknown', 'access.refused', path, { reason }), + }, + ], + } + } +} diff --git a/api/src/bank-connections/banking-unavailable.controller.ts b/api/src/bank-connections/banking-unavailable.controller.ts new file mode 100644 index 0000000..d3a8dda --- /dev/null +++ b/api/src/bank-connections/banking-unavailable.controller.ts @@ -0,0 +1,23 @@ +import { All, Controller, Get, Inject, NotFoundException, ServiceUnavailableException } from '@nestjs/common' +import { BANKING_STATE, BankingState } from './config/bank-connections.tokens' + +/** Stands in for the real banking routes when banking is off or set up incorrectly (it fails closed). */ +@Controller('api/banking') +export class BankingUnavailableController { + constructor(@Inject(BANKING_STATE) private readonly state: BankingState) {} + + @Get('status') + status() { + return { enabled: false } + } + + @All('{*path}') + everythingElse(): never { + if (this.state === 'off') { + throw new NotFoundException({ error: 'Not Found' }) + } + throw new ServiceUnavailableException({ + error: 'Banking is not available', + }) + } +} diff --git a/api/src/bank-connections/config/bank-connections.tokens.ts b/api/src/bank-connections/config/bank-connections.tokens.ts new file mode 100644 index 0000000..37673d3 --- /dev/null +++ b/api/src/bank-connections/config/bank-connections.tokens.ts @@ -0,0 +1,10 @@ +import { BankingConfig } from './banking.config' + +export const BANKING_CONFIG = Symbol('BANKING_CONFIG') +export const BANKING_STATE = Symbol('BANKING_STATE') +export type BankingState = 'off' | 'misconfigured' +export const CLOCK = Symbol('CLOCK') +export interface Clock { + now(): Date +} +export type { BankingConfig } diff --git a/api/src/bank-connections/config/banking.config.test.ts b/api/src/bank-connections/config/banking.config.test.ts new file mode 100644 index 0000000..ae5b867 --- /dev/null +++ b/api/src/bank-connections/config/banking.config.test.ts @@ -0,0 +1,88 @@ +import { describe, it, expect } from 'vitest' +import { loadAccessSettings } from '../../access/access.settings' +import { loadBankingConfig } from './banking.config' + +const KEY = Buffer.alloc(32, 9) + +describe('config fails closed', () => { + const prod = { NODE_ENV: 'production', ALLOWED_ORIGIN: 'https://finkit.test' } + + it('is off by default', () => expect(loadBankingConfig({}).config).toBeUndefined()) + it('rejects the mock bank in production', () => { + expect( + loadBankingConfig({ + ...prod, + BANKING_PROVIDER: 'mock', + CF_ACCESS_TEAM_DOMAIN: 't', + CF_ACCESS_AUD: 'a', + BANKING_ENCRYPTION_KEY: KEY.toString('base64'), + }).errors.join(), + ).toMatch(/mock/i) + }) + it('requires a real encryption key in production', () => { + expect( + loadBankingConfig({ + ...prod, + BANKING_PROVIDER: 'enablebanking', + EB_APP_ID: 'x', + EB_PRIVATE_KEY: 'k', + BANKING_REDIRECT_URL: 'https://a.b/cb', + }).errors.join(), + ).toMatch(/ENCRYPTION_KEY/) + }) + it('requires https for the return address in production', () => { + expect( + loadBankingConfig({ + ...prod, + BANKING_PROVIDER: 'enablebanking', + EB_APP_ID: 'x', + EB_PRIVATE_KEY: 'k', + BANKING_REDIRECT_URL: 'http://a.b/cb', + CF_ACCESS_TEAM_DOMAIN: 't', + CF_ACCESS_AUD: 'a', + BANKING_ENCRYPTION_KEY: KEY.toString('base64'), + }).errors.join(), + ).toMatch(/https/) + }) + it('accepts a complete production setup', () => { + const r = loadBankingConfig({ + ...prod, + BANKING_PROVIDER: 'enablebanking', + EB_APP_ID: 'app', + EB_PRIVATE_KEY: 'k', + BANKING_REDIRECT_URL: 'https://a.b/cb', + CF_ACCESS_TEAM_DOMAIN: 't', + CF_ACCESS_AUD: 'a', + BANKING_ENCRYPTION_KEY: KEY.toString('base64'), + BANKING_ADMIN_EMAILS: 'Me@X.com', + }) + + expect(r.errors).toEqual([]) + const access = loadAccessSettings({ + ...prod, + CF_ACCESS_TEAM_DOMAIN: 't', + CF_ACCESS_AUD: 'a', + BANKING_ADMIN_EMAILS: 'Me@X.com', + }) + + expect(access.mode).toBe('cloudflare') + // BANKING_ADMIN_EMAILS is the old name for the allowed list + expect(access.allowedEmails).toEqual(['me@x.com']) + }) +}) + +describe('sign-in settings', () => { + it('is development mode (you are dev@localhost) without Cloudflare values', () => { + expect(loadAccessSettings({}).mode).toBe('dev') + }) + it('lets the first listed address inherit old data, however people arrive', () => { + const a = loadAccessSettings({ + CF_ACCESS_TEAM_DOMAIN: 't', + CF_ACCESS_AUD: 'a', + ALLOWED_USER_EMAILS: 'B@x.com, c@x.com', + }) + + expect(a.allowedEmails).toEqual(['b@x.com', 'c@x.com']) + expect(a.legacyOwner).toBe('b@x.com') + }) +}) diff --git a/api/src/bank-connections/config/banking.config.ts b/api/src/bank-connections/config/banking.config.ts new file mode 100644 index 0000000..6de57cb --- /dev/null +++ b/api/src/bank-connections/config/banking.config.ts @@ -0,0 +1,128 @@ +import { readFileSync } from 'node:fs' + +export type ProviderMode = 'off' | 'mock' | 'enablebanking' + +export interface BankingConfig { + provider: ProviderMode + isProd: boolean + encryptionKey: Buffer + usesDevKey: boolean + redirectUrl: string + // Access control + accessMode: 'cloudflare' | 'dev' + cfTeamDomain?: string + cfAudience?: string + allowedOrigin?: string + /** Hours between scheduled catch-up syncs; 0 turns them off */ + autoSyncHours: number + // Enable Banking + ebAppId?: string + ebPrivateKey?: string +} + +interface ConfigResult { + config?: BankingConfig + errors: string[] // why banking is unavailable (logged, never shown to clients) +} + +// A fixed key for local development only. Never accepted in production. +const DEV_KEY = Buffer.alloc(32, 7) + +/** + * Banking fails closed: in production it refuses to run unless access control, a real + * encryption key and (for the real provider) credentials are all configured. + */ +export function loadBankingConfig(env: NodeJS.ProcessEnv = process.env): ConfigResult { + const errors: string[] = [] + const provider = (env.BANKING_PROVIDER || 'off') as ProviderMode + + if (!['off', 'mock', 'enablebanking'].includes(provider)) { + return { + errors: [`BANKING_PROVIDER must be off, mock or enablebanking (got "${provider}")`], + } + } + if (provider === 'off') { + return { errors: [] } + } + + const isProd = env.NODE_ENV === 'production' + + if (provider === 'mock' && isProd && env.BANKING_ALLOW_MOCK_IN_PROD !== '1') { + errors.push('The mock provider is not allowed in production') + } + + let encryptionKey: Buffer + + if (env.BANKING_ENCRYPTION_KEY) { + encryptionKey = Buffer.from(env.BANKING_ENCRYPTION_KEY, 'base64') + if (encryptionKey.length !== 32) { + errors.push('BANKING_ENCRYPTION_KEY must be 32 bytes, base64 encoded') + } + } else if (!isProd) { + encryptionKey = DEV_KEY + } else { + encryptionKey = DEV_KEY + errors.push('BANKING_ENCRYPTION_KEY is required in production') + } + + const cfTeamDomain = env.CF_ACCESS_TEAM_DOMAIN + const cfAudience = env.CF_ACCESS_AUD + const accessMode: 'cloudflare' | 'dev' = cfTeamDomain && cfAudience ? 'cloudflare' : 'dev' + + if (isProd && accessMode !== 'cloudflare') { + errors.push('CF_ACCESS_TEAM_DOMAIN and CF_ACCESS_AUD are required in production') + } + const redirectUrl = env.BANKING_REDIRECT_URL || '/api/banking/callback' + + if (provider === 'enablebanking') { + if (!redirectUrl.startsWith('https://') && isProd) { + errors.push('BANKING_REDIRECT_URL must be an https URL') + } + } + + let ebPrivateKey: string | undefined + + if (provider === 'enablebanking') { + if (!env.EB_APP_ID) { + errors.push('EB_APP_ID is required') + } + try { + ebPrivateKey = + env.EB_PRIVATE_KEY || (env.EB_PRIVATE_KEY_PATH ? readFileSync(env.EB_PRIVATE_KEY_PATH, 'utf8') : undefined) + } catch { + errors.push('Could not read EB_PRIVATE_KEY_PATH') + } + if (!ebPrivateKey) { + errors.push('EB_PRIVATE_KEY_PATH (or EB_PRIVATE_KEY) is required') + } + } + + if (errors.length) { + return { errors } + } + + return { + errors, + config: { + provider, + isProd, + encryptionKey, + usesDevKey: !env.BANKING_ENCRYPTION_KEY, + redirectUrl, + accessMode, + cfTeamDomain, + cfAudience, + allowedOrigin: env.ALLOWED_ORIGIN, + autoSyncHours: parseHours(env.BANKING_AUTO_SYNC_HOURS), + ebAppId: env.EB_APP_ID, + ebPrivateKey, + }, + } +} + +/** Default 12 (twice a day, well inside the banks' limit for access without the user present). 0 = off. */ +function parseHours(value?: string): number { + const n = value === undefined || value === '' ? 12 : Number(value) + + return Number.isFinite(n) && n >= 0 ? (n === 0 ? 0 : Math.max(6, n)) : 12 +} diff --git a/api/src/bank-connections/connections/bank-callback.controller.ts b/api/src/bank-connections/connections/bank-callback.controller.ts new file mode 100644 index 0000000..2ea39e3 --- /dev/null +++ b/api/src/bank-connections/connections/bank-callback.controller.ts @@ -0,0 +1,48 @@ +import { Controller, Get, Query, Req, Res, UseInterceptors } from '@nestjs/common' +import type { Request, Response } from 'express' +import { NoStoreInterceptor } from '../../shared/http/no-store.interceptor' +import { BankConnectionsService } from './bank-connections.service' +import { STATE_COOKIE } from './bank-connections.controller' + +function cookieValue(req: Request, name: string): string | undefined { + for (const part of (req.headers.cookie || '').split(';')) { + const [k, ...v] = part.trim().split('=') + + if (k === name) { + return decodeURIComponent(v.join('=')) + } + } + + return undefined +} + +/** + * Where the bank sends the user back. This is the only public banking route: it is protected by a + * single-use request we started plus a cookie from the same browser, not by sign-in. + */ +@Controller('api/banking') +@UseInterceptors(NoStoreInterceptor) +export class BankCallbackController { + constructor(private readonly connections: BankConnectionsService) {} + + @Get('callback') + async callback( + @Query('state') state: string, + @Query('code') code: string | undefined, + @Query('error') error: string | undefined, + @Req() req: Request, + @Res() res: Response, + ) { + const cookieState = cookieValue(req, STATE_COOKIE) + + res.clearCookie(STATE_COOKIE, { path: '/api/banking' }) + const outcome = await this.connections.completeConnection({ + state, + code, + error, + cookieState, + }) + + return res.redirect(302, BankConnectionsService.returnUrl(outcome)) + } +} diff --git a/api/src/bank-connections/connections/bank-connections.controller.ts b/api/src/bank-connections/connections/bank-connections.controller.ts new file mode 100644 index 0000000..f52e02f --- /dev/null +++ b/api/src/bank-connections/connections/bank-connections.controller.ts @@ -0,0 +1,81 @@ +import { + HttpCode, + Body, + Controller, + Delete, + Get, + Param, + Post, + Query, + Res, + UseGuards, + UseInterceptors, + BadRequestException, +} from '@nestjs/common' +import type { Response } from 'express' +import { AccessGuard, CurrentActor } from '../../access/access.guard' +import { Actor } from '../../access/access.settings' +import { CsrfGuard } from '../../access/csrf.guard' +import { NoStoreInterceptor } from '../../shared/http/no-store.interceptor' +import { ZodValidationPipe } from '../../shared/http/zod-validation.pipe' +import { BankConnectionsService } from './bank-connections.service' +import { ConnectInput, ConnectSchema } from './bank-connections.schemas' +import { OAUTH_STATE_TTL_MS } from '../constants' + +export const STATE_COOKIE = 'finkit_bank_state' + +/** What a signed-in user does with their bank connections. */ +@Controller('api/banking') +@UseGuards(AccessGuard, CsrfGuard) +@UseInterceptors(NoStoreInterceptor) +export class BankConnectionsController { + constructor(private readonly connections: BankConnectionsService) {} + + @Get('status') + status() { + return this.connections.status() + } + + @Get('banks') + async banks(@Query('country') country = 'DE') { + const code = country.toUpperCase() + + if (!/^[A-Z]{2}$/.test(code)) { + throw new BadRequestException({ error: 'Invalid country' }) + } + + return { banks: await this.connections.banks(code) } + } + + @Post('connect') + @HttpCode(200) // the previous API answered 200, not 201 + async connect( + @CurrentActor() actor: Actor, + @Body(new ZodValidationPipe(ConnectSchema, { message: 'Invalid request' })) + body: ConnectInput, + @Res({ passthrough: true }) res: Response, + ) { + const { url, state } = await this.connections.startConnection(actor, body) + + // Ties the bank's return to this browser: the callback must present the same value + res.cookie(STATE_COOKIE, state, { + httpOnly: true, + sameSite: 'lax', + secure: this.connections.config.isProd, + path: '/api/banking', + maxAge: OAUTH_STATE_TTL_MS, + }) + + return { url } + } + + @Get('connections') + list(@CurrentActor() actor: Actor) { + return { connections: this.connections.list(actor) } + } + + @Delete('connections/:id') + disconnect(@CurrentActor() actor: Actor, @Param('id') id: string) { + return this.connections.disconnect(actor, id) + } +} diff --git a/api/src/bank-connections/connections/bank-connections.schemas.ts b/api/src/bank-connections/connections/bank-connections.schemas.ts new file mode 100644 index 0000000..9c54079 --- /dev/null +++ b/api/src/bank-connections/connections/bank-connections.schemas.ts @@ -0,0 +1,16 @@ +import { z } from 'zod' + +export const ConnectSchema = z.object({ + bank: z.string().min(1).max(100), + country: z.string().length(2).default('DE'), + reconnect: z.string().uuid().optional(), +}) +export type ConnectInput = z.infer + +const day = z.string().regex(/^\d{4}-\d{2}-\d{2}$/) + +export const SyncSchema = z.object({ + from: day.optional(), + to: day.optional(), +}) +export type SyncInput = z.infer diff --git a/api/src/bank-connections/connections/bank-connections.service.ts b/api/src/bank-connections/connections/bank-connections.service.ts new file mode 100644 index 0000000..b76246f --- /dev/null +++ b/api/src/bank-connections/connections/bank-connections.service.ts @@ -0,0 +1,381 @@ +import { + BadGatewayException, + BadRequestException, + HttpException, + Inject, + Injectable, + NotFoundException, + OnModuleInit, +} from '@nestjs/common' +import { ACCESS_SETTINGS, AccessSettings, Actor } from '../../access/access.settings' +import { accountRef, decrypt, encrypt, randomToken } from '../../shared/crypto/encryption' +import { BANK_PROVIDER, BankProvider, ProviderError } from '../providers/bank-provider' +import { BANKING_CONFIG, BankingConfig, CLOCK, Clock } from '../config/bank-connections.tokens' +import { BankConnectionsStore, ConnectionRow } from './bank-connections.store' +import { BankTransactionsStore } from '../transactions/bank-transactions.store' +import { ConnectInput } from './bank-connections.schemas' +import { EXPIRING_DAYS, connectionStatus } from './connection-status' +import { BANK_LIST_CACHE_TTL_MS, CONNECT_RATE_LIMIT_MAX_ATTEMPTS, CONNECT_RATE_LIMIT_WINDOW_MS } from '../constants' +import { addDays } from '../../shared/constants/time' +import { RateLimiter } from './rate-limiter' + +const BANKS_URL = '/app/banks' // where the bank sends the user back to + +/** Why the bank's return didn't produce a connection (shown to the user as a friendly message). */ +type CallbackOutcome = + | { ok: true; connectionId: string } + | { ok: false; reason: 'invalid' | 'expired' | 'denied' | 'failed' } + +@Injectable() +export class BankConnectionsService implements OnModuleInit { + private readonly limiter: RateLimiter + private readonly bankCache = new Map> }>() + + constructor( + private readonly store: BankConnectionsStore, + private readonly txs: BankTransactionsStore, + @Inject(BANK_PROVIDER) readonly provider: BankProvider, + @Inject(BANKING_CONFIG) readonly config: BankingConfig, + @Inject(ACCESS_SETTINGS) private readonly access: AccessSettings, + @Inject(CLOCK) private readonly clock: Clock, + ) { + this.limiter = new RateLimiter(() => this.clock.now().getTime()) + } + + onModuleInit() { + this.store.prune() + } + + private now() { + return this.clock.now() + } + + // ---------------- views ---------------- + view(row: ConnectionRow) { + const { status, daysLeft } = connectionStatus(row, this.now()) + const accounts = JSON.parse(row.account_labels) as Array<{ + id: string + label: string + currency?: string + ownIban?: boolean + }> + + return { + id: row.id, + bank: row.aspsp_name, + country: row.aspsp_country, + status, + daysLeft, + validUntil: row.valid_until, + accounts: accounts.map(a => ({ id: a.id, label: a.label, currency: a.currency })), + // Made before we kept the account's own IBAN: reconnecting lets transfers between accounts be spotted + reconnectSuggested: accounts.some(a => a.ownIban === undefined), + lastSyncAt: row.last_sync_at, + lastSyncTo: row.last_sync_to, + createdAt: row.created_at, + } + } + + status() { + return { + enabled: true, + provider: this.provider.name, + mock: this.provider.name === 'mock', + expiringDays: EXPIRING_DAYS, + } + } + + async banks(country: string) { + const hit = this.bankCache.get(country) + + if (hit && this.now().getTime() - hit.at < BANK_LIST_CACHE_TTL_MS) { + return hit.banks + } + try { + const banks = await this.provider.listBanks(country) + + this.bankCache.set(country, { at: this.now().getTime(), banks }) + + return banks + } catch { + throw new BadGatewayException({ error: 'Could not load banks' }) + } + } + + list(actor: Actor) { + return this.store.listConnections(actor.userId).map(r => this.view(r)) + } + + // ---------------- connect ---------------- + async startConnection(actor: Actor, input: ConnectInput): Promise<{ url: string; state: string }> { + if (!this.limiter.allow(`connect:${actor.email}`, CONNECT_RATE_LIMIT_WINDOW_MS, CONNECT_RATE_LIMIT_MAX_ATTEMPTS)) { + throw new HttpException({ error: 'Too many attempts. Try again later.' }, 429) + } + if (input.reconnect && !this.store.getOwnedConnection(input.reconnect, actor.userId)) { + throw new NotFoundException({ error: 'Unknown connection' }) + } + + const bank = (await this.banks(input.country.toUpperCase()).catch(() => [])).find(b => b.name === input.bank) + + if (!bank) { + throw new BadRequestException({ error: 'Unknown bank' }) + } + + // One login covers all of a bank's accounts, so connecting a bank you already have replaces + // the old link instead of adding a duplicate card. + const existing = this.store + .listConnections(actor.userId) + .find(c => c.aspsp_name === bank.name && c.aspsp_country === bank.country) + const replaces = input.reconnect ?? existing?.id + + const state = randomToken(32) + const validUntil = addDays(this.now(), Math.max(1, Math.min(bank.maxConsentDays, 180)) - 0.05) + + try { + const { url } = await this.provider.startAuth({ + bank, + redirectUrl: this.config.redirectUrl, + state, + validUntil, + }) + + this.store.createState({ + state, + userId: actor.userId, + bankName: bank.name, + country: bank.country, + replaceConnectionId: replaces, + }) + this.store.audit(actor.email, 'connect.start', bank.name, replaces ? { replaces } : undefined, actor.userId) + + return { url, state } + } catch { + throw new BadGatewayException({ + error: 'Could not start the connection', + }) + } + } + + /** The bank sent the user back. Valid only for a request we started, from the same browser, once. */ + async completeConnection(p: { + state: string + code?: string + error?: string + cookieState?: string + }): Promise { + if (!p.state || !p.cookieState || p.cookieState !== p.state) { + this.store.audit('callback', 'connect.failed', undefined, { + reason: 'state mismatch', + }) + + return { ok: false, reason: 'invalid' } + } + const pending = this.store.consumeState(p.state) + + if (!pending) { + this.store.audit('callback', 'connect.failed', undefined, { + reason: 'expired or reused', + }) + + return { ok: false, reason: 'expired' } + } + if (p.error || !p.code) { + this.store.audit( + 'callback', + 'connect.denied', + pending.aspsp_name, + { error: p.error || 'no code' }, + pending.user_id, + ) + + return { ok: false, reason: 'denied' } + } + try { + const session = await this.provider.completeAuth(p.code) + // What this bank needs to treat a Sync press as a person being present (see PSU headers) + const bank = (await this.banks(pending.aspsp_country).catch(() => [])).find(b => b.name === pending.aspsp_name) + + const old = pending.replace_connection_id ? this.store.getConnection(pending.replace_connection_id) : undefined + const id = this.store.createConnection({ + // The bank sends the browser back without a sign-in, so the owner is whoever started this request + userId: pending.user_id!, + provider: this.provider.name, + bankName: pending.aspsp_name, + country: pending.aspsp_country, + secretBlob: encrypt( + JSON.stringify({ + sessionId: session.sessionId, + accounts: session.accounts, + requiredPsuHeaders: bank?.requiredPsuHeaders, + }), + this.config.encryptionKey, + ), + accountLabels: session.accounts.map(a => ({ + id: accountRef(a.stableId), + label: a.label, + currency: a.currency, + ownIban: !!a.iban, // lets old connections (made before this was kept) be told apart + })), + validUntil: session.validUntil, + lastSyncTo: old?.last_sync_to ?? null, // keep syncing from where the old connection stopped + }) + + if (old && old.status !== 'revoked') { + await this.revokeAtBank(old) + this.store.retire(old.id, 'replaced') + // Same accounts, new consent: the history carries over + this.txs.moveConnection(pending.user_id!, old.id, id) + } + this.store.audit( + 'callback', + 'connect.complete', + pending.aspsp_name, + { + accounts: session.accounts.length, + connection: id, + }, + pending.user_id, + ) + + return { ok: true, connectionId: id } + } catch (e) { + this.store.audit( + 'callback', + 'connect.failed', + pending.aspsp_name, + { + reason: e instanceof ProviderError ? e.kind : 'error', + }, + pending.user_id, + ) + + return { ok: false, reason: 'failed' } + } + } + + static returnUrl(outcome: CallbackOutcome): string { + return outcome.ok + ? `${BANKS_URL}?bank=connected&c=${outcome.connectionId}` + : `${BANKS_URL}?bank=error&reason=${outcome.reason}` + } + + async disconnect(actor: Actor, id: string) { + const row = this.store.getOwnedConnection(id, actor.userId) + + if (!row) { + throw new NotFoundException({ error: 'Not found' }) + } + await this.revokeAtBank(row) + this.store.retire(row.id, 'revoked') + // Disconnecting removes what was synced through it + const deleted = this.txs.deleteForConnection(actor.userId, row.id) + + this.store.audit(actor.email, 'disconnect', row.aspsp_name, { deleted }, actor.userId) + + return { ok: true } + } + + private async revokeAtBank(row: ConnectionRow) { + try { + const s = JSON.parse(decrypt(row.secret_blob, this.config.encryptionKey)) as { sessionId: string } + + await this.provider.revoke(s.sessionId) + } catch { + /* already gone or nothing stored */ + } + } + + // ---------------- back office ---------------- + overview(actor: Actor) { + const conns = this.store.listConnections(actor.userId).map(r => this.view(r)) + const last = this.store.lastSuccessfulSync(actor.userId) + const { access, config, provider } = this + + return { + provider: provider.name, + connections: { + total: conns.length, + active: conns.filter(c => c.status === 'active').length, + expiring: conns.filter(c => c.status === 'expiring').length, + expired: conns.filter(c => c.status === 'expired').length, + }, + syncs30d: this.store.syncStats(actor.userId, 30), + lastSuccessfulSync: last ? { at: last.finished_at, bank: last.bank } : null, + checks: [ + { + id: 'provider', + ok: true, + label: 'Bank provider', + detail: provider.name === 'mock' ? 'Test bank (development)' : 'Enable Banking', + }, + { + id: 'signing', + ok: provider.name === 'mock' || !!config.ebPrivateKey, + label: 'Request signing key loaded', + detail: + provider.name === 'mock' + ? 'not needed for the test bank' + : `RS256 · app id …${(config.ebAppId || '').slice(-4)}`, + }, + { + id: 'encryption', + ok: !config.usesDevKey, + label: 'Stored links are encrypted with your own key', + detail: config.usesDevKey ? 'using the development key' : 'AES-256-GCM', + }, + { + id: 'access', + ok: access.mode === 'cloudflare', + label: 'Sign-in required (Cloudflare Access)', + detail: access.mode === 'cloudflare' ? 'token verified on every request' : 'development mode: no sign-in', + }, + { + id: 'https', + ok: config.redirectUrl.startsWith('https://'), + label: 'Return address uses https', + detail: config.redirectUrl, + }, + { + id: 'mock', + ok: !(config.isProd && provider.name === 'mock'), + label: 'Test bank is blocked in production', + detail: provider.name === 'mock' ? 'test bank is active (development)' : 'refused at startup', + }, + { + id: 'people', + ok: access.allowedEmails.length > 0, + label: `${access.allowedEmails.length || 'No'} ${access.allowedEmails.length === 1 ? 'person is' : 'people are'} allowed in`, + detail: access.mode === 'dev' ? 'development: no sign-in' : 'each with their own separate data', + }, + ], + } + } + + syncLog(actor: Actor, limit: number) { + return { + entries: this.store.syncLog(actor.userId, Math.min(limit || 100, 500)), + } + } + + auditLog(actor: Actor, limit: number) { + return { + entries: this.store.auditLog(actor.userId, Math.min(limit || 100, 500)), + } + } + + /** Test bank only: ages a connection so the expiring and expired states can be seen. */ + ageConnection(actor: Actor, id: string, mode: 'expiring' | 'expired') { + const row = this.store.getOwnedConnection(id, actor.userId) + + if (!row) { + throw new NotFoundException({ error: 'Not found' }) + } + this.store.setValidUntil(row.id, addDays(this.now(), mode === 'expired' ? -1 : 9).toISOString()) + if (mode === 'expired') { + this.store.setStatus(row.id, 'expired') + } + this.store.audit(actor.email, 'test.expire', row.aspsp_name, { mode }, actor.userId) + + return { ok: true } + } +} diff --git a/api/src/bank-connections/connections/bank-connections.store.ts b/api/src/bank-connections/connections/bank-connections.store.ts new file mode 100644 index 0000000..96a6d34 --- /dev/null +++ b/api/src/bank-connections/connections/bank-connections.store.ts @@ -0,0 +1,310 @@ +import { Injectable } from '@nestjs/common' +import { randomUUID } from 'node:crypto' +import { isoDaysAgo } from '../../shared/constants/time' +import { AUDIT_LOG_RETENTION_DAYS, OAUTH_STATE_TTL_MS, SYNC_LOG_RETENTION_DAYS } from '../constants' +import { DatabaseService } from '../../shared/database/database.service' +import { ensureColumn } from '../../shared/database/migrate' +import { UsersService } from '../../users/users.service' + +export type ConnectionStatus = 'active' | 'expired' | 'revoked' | 'replaced' + +export interface ConnectionRow { + id: string + user_id: string | null + provider: string + aspsp_name: string + aspsp_country: string + status: ConnectionStatus + secret_blob: string // encrypted {sessionId, accounts} + account_labels: string // JSON: [{label, currency}] (masked, safe to show) + valid_until: string + created_at: string + last_sync_at: string | null + last_sync_started_at: string | null + last_sync_to: string | null +} + +interface AuthState { + state: string + user_id: string | null + aspsp_name: string + aspsp_country: string + expires_at: string + used: number + replace_connection_id: string | null +} + +const iso = () => new Date().toISOString() + +/** All persistence for bank connections: metadata only. Synced transactions live in BankTransactionsStore. */ +@Injectable() +export class BankConnectionsStore { + private readonly q + + constructor(database: DatabaseService, users: UsersService) { + const { db } = database + + db.exec(` + CREATE TABLE IF NOT EXISTS bank_connections ( + id TEXT PRIMARY KEY, + provider TEXT NOT NULL, + aspsp_name TEXT NOT NULL, + aspsp_country TEXT NOT NULL, + status TEXT NOT NULL, + secret_blob TEXT NOT NULL, + account_labels TEXT NOT NULL, + valid_until TEXT NOT NULL, + created_at TEXT NOT NULL, + last_sync_at TEXT, + last_sync_started_at TEXT, + last_sync_to TEXT + ); + CREATE TABLE IF NOT EXISTS bank_auth_states ( + state TEXT PRIMARY KEY, + aspsp_name TEXT NOT NULL, + aspsp_country TEXT NOT NULL, + expires_at TEXT NOT NULL, + used INTEGER NOT NULL DEFAULT 0, + replace_connection_id TEXT + ); + CREATE TABLE IF NOT EXISTS bank_sync_log ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + connection_id TEXT NOT NULL, + started_at TEXT NOT NULL, + finished_at TEXT, + date_from TEXT, + date_to TEXT, + status TEXT NOT NULL, + fetched INTEGER NOT NULL DEFAULT 0, + error TEXT + ); + CREATE TABLE IF NOT EXISTS bank_audit_log ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + ts TEXT NOT NULL, + actor TEXT NOT NULL, + action TEXT NOT NULL, + target TEXT, + detail TEXT + ); + CREATE INDEX IF NOT EXISTS idx_bank_sync_log_started ON bank_sync_log(started_at); + CREATE INDEX IF NOT EXISTS idx_bank_audit_ts ON bank_audit_log(ts); + `) + // Older databases predate user accounts: add the owner column, and let the first user adopt old rows + ensureColumn(db, 'bank_connections', 'user_id', 'TEXT') + ensureColumn(db, 'bank_auth_states', 'user_id', 'TEXT') + ensureColumn(db, 'bank_audit_log', 'user_id', 'TEXT') + db.exec('CREATE INDEX IF NOT EXISTS idx_bank_connections_user ON bank_connections(user_id)') + users.onLegacyOwnerCreated(userId => { + db.prepare('UPDATE bank_connections SET user_id = ? WHERE user_id IS NULL').run(userId) + db.prepare('UPDATE bank_auth_states SET user_id = ? WHERE user_id IS NULL').run(userId) + db.prepare('UPDATE bank_audit_log SET user_id = ? WHERE user_id IS NULL').run(userId) + }) + this.q = { + insertState: db.prepare( + 'INSERT INTO bank_auth_states (state, user_id, aspsp_name, aspsp_country, expires_at, replace_connection_id) VALUES (?,?,?,?,?,?)', + ), + getState: db.prepare('SELECT * FROM bank_auth_states WHERE state = ?'), + // Single-use: only one caller can flip used from 0 to 1 + useState: db.prepare('UPDATE bank_auth_states SET used = 1 WHERE state = ? AND used = 0'), + purgeStates: db.prepare('DELETE FROM bank_auth_states WHERE expires_at < ?'), + insertConn: db.prepare(`INSERT INTO bank_connections + (id, user_id, provider, aspsp_name, aspsp_country, status, secret_blob, account_labels, valid_until, created_at, last_sync_to) + VALUES (@id, @user_id, @provider, @aspsp_name, @aspsp_country, 'active', @secret_blob, @account_labels, @valid_until, @created_at, @last_sync_to)`), + getConn: db.prepare('SELECT * FROM bank_connections WHERE id = ?'), + listConns: db.prepare( + "SELECT * FROM bank_connections WHERE user_id = ? AND status NOT IN ('revoked','replaced') ORDER BY created_at DESC", + ), + due: db.prepare( + `SELECT * FROM bank_connections WHERE status = 'active' AND user_id IS NOT NULL AND secret_blob != '' + AND valid_until > ? AND (last_sync_started_at IS NULL OR last_sync_started_at < ?)`, + ), + setStatus: db.prepare('UPDATE bank_connections SET status = ? WHERE id = ?'), + wipeSecret: db.prepare("UPDATE bank_connections SET status = ?, secret_blob = '' WHERE id = ?"), + syncStarted: db.prepare('UPDATE bank_connections SET last_sync_started_at = ? WHERE id = ?'), + syncDone: db.prepare('UPDATE bank_connections SET last_sync_at = ?, last_sync_to = ? WHERE id = ?'), + setValidUntil: db.prepare('UPDATE bank_connections SET valid_until = ? WHERE id = ?'), + insertSync: db.prepare( + 'INSERT INTO bank_sync_log (connection_id, started_at, date_from, date_to, status) VALUES (?,?,?,?,?)', + ), + finishSync: db.prepare( + 'UPDATE bank_sync_log SET finished_at = ?, status = ?, fetched = ?, error = ? WHERE id = ?', + ), + syncLog: + db.prepare(`SELECT l.id, l.connection_id, c.aspsp_name AS bank, l.started_at, l.finished_at, l.date_from, l.date_to, l.status, l.fetched, l.error + FROM bank_sync_log l JOIN bank_connections c ON c.id = l.connection_id WHERE c.user_id = ? ORDER BY l.id DESC LIMIT ?`), + syncStats: + db.prepare(`SELECT l.status, COUNT(*) AS n FROM bank_sync_log l JOIN bank_connections c ON c.id = l.connection_id + WHERE c.user_id = ? AND l.started_at >= ? GROUP BY l.status`), + lastOk: + db.prepare(`SELECT l.finished_at, c.aspsp_name AS bank FROM bank_sync_log l JOIN bank_connections c ON c.id = l.connection_id + WHERE c.user_id = ? AND l.status = 'ok' ORDER BY l.id DESC LIMIT 1`), + insertAudit: db.prepare( + 'INSERT INTO bank_audit_log (ts, actor, action, target, detail, user_id) VALUES (?,?,?,?,?,?)', + ), + audit: db.prepare( + 'SELECT ts, actor, action, target, detail FROM bank_audit_log WHERE user_id = ? ORDER BY id DESC LIMIT ?', + ), + pruneSync: db.prepare('DELETE FROM bank_sync_log WHERE started_at < ?'), + pruneAudit: db.prepare('DELETE FROM bank_audit_log WHERE ts < ?'), + } + } + + // ---- sign-in requests started by the user (single use, ten minutes) ---- + createState(p: { + state: string + userId: string + bankName: string + country: string + replaceConnectionId?: string + ttlMs?: number + }) { + this.q.purgeStates.run(iso()) + this.q.insertState.run( + p.state, + p.userId, + p.bankName, + p.country, + new Date(Date.now() + (p.ttlMs ?? OAUTH_STATE_TTL_MS)).toISOString(), + p.replaceConnectionId ?? null, + ) + } + + getState(state: string) { + return this.q.getState.get(state) as AuthState | undefined + } + + /** Returns the request only for the first caller within its validity window. */ + consumeState(state: string): AuthState | null { + const row = this.getState(state) + + if (!row || row.used || row.expires_at < iso()) { + return null + } + + return this.q.useState.run(state).changes === 1 ? row : null + } + + // ---- connections ---- + createConnection(p: { + userId: string + provider: string + bankName: string + country: string + secretBlob: string + accountLabels: unknown + validUntil: string + lastSyncTo?: string | null + }) { + const id = randomUUID() + + this.q.insertConn.run({ + id, + user_id: p.userId, + provider: p.provider, + aspsp_name: p.bankName, + aspsp_country: p.country, + secret_blob: p.secretBlob, + account_labels: JSON.stringify(p.accountLabels), + valid_until: p.validUntil, + created_at: iso(), + last_sync_to: p.lastSyncTo ?? null, + }) + + return id + } + + /** Any connection by id. Callers that act for a user must also check `ownedBy`. */ + getConnection(id: string) { + return this.q.getConn.get(id) as ConnectionRow | undefined + } + + /** The connection, only when it belongs to this user. */ + getOwnedConnection(id: string, userId: string) { + const row = this.getConnection(id) + + return row && row.user_id === userId ? row : undefined + } + + listConnections(userId: string) { + return this.q.listConns.all(userId) as ConnectionRow[] + } + + /** Active connections not synced since `before`, for the scheduled catch-up. */ + dueForSync(now: string, before: string) { + return this.q.due.all(now, before) as ConnectionRow[] + } + + setStatus(id: string, status: ConnectionStatus) { + this.q.setStatus.run(status, id) + } + + retire(id: string, status: 'revoked' | 'replaced') { + this.q.wipeSecret.run(status, id) + } // also erases the stored link + + markSyncStarted(id: string, at: string = iso()) { + this.q.syncStarted.run(at, id) + } + + markSyncDone(id: string, to: string, at: string = iso()) { + this.q.syncDone.run(at, to, id) + } + + setValidUntil(id: string, validUntil: string) { + this.q.setValidUntil.run(validUntil, id) + } + + // ---- sync log and audit trail ---- + startSyncLog(connectionId: string, from: string, to: string) { + return Number(this.q.insertSync.run(connectionId, iso(), from, to, 'running').lastInsertRowid) + } + + finishSyncLog(id: number, status: 'ok' | 'failed' | 'rate_limited', fetched: number, error?: string) { + this.q.finishSync.run(iso(), status, fetched, error ? error.slice(0, 200) : null, id) + } + + syncLog(userId: string, limit = 100) { + return this.q.syncLog.all(userId, limit) as Array> + } + + syncStats(userId: string, days = 30) { + const rows = this.q.syncStats.all(userId, isoDaysAgo(days)) as Array<{ + status: string + n: number + }> + const by = Object.fromEntries(rows.map(r => [r.status, r.n])) + + return { + ok: by.ok ?? 0, + failed: (by.failed ?? 0) + (by.rate_limited ?? 0), + total: rows.reduce((sum, r) => sum + r.n, 0), + } + } + + lastSuccessfulSync(userId: string) { + return this.q.lastOk.get(userId) as { finished_at: string; bank: string } | undefined + } + + /** `userId` is whose trail this belongs to. Entries with none (refused strangers) are nobody's. */ + audit(actor: string, action: string, target?: string, detail?: unknown, userId?: string | null) { + this.q.insertAudit.run( + iso(), + actor, + action, + target ?? null, + detail === undefined ? null : JSON.stringify(detail).slice(0, 500), + userId ?? null, + ) + } + + /** What happened to this person's connections. */ + auditLog(userId: string, limit = 100) { + return this.q.audit.all(userId, limit) as Array> + } + + prune() { + this.q.pruneSync.run(isoDaysAgo(SYNC_LOG_RETENTION_DAYS)) + this.q.pruneAudit.run(isoDaysAgo(AUDIT_LOG_RETENTION_DAYS)) + this.q.purgeStates.run(iso()) + } +} diff --git a/api/src/bank-connections/connections/bank-connections.test.ts b/api/src/bank-connections/connections/bank-connections.test.ts new file mode 100644 index 0000000..92c698f --- /dev/null +++ b/api/src/bank-connections/connections/bank-connections.test.ts @@ -0,0 +1,524 @@ +import { afterEach, describe, expect, it } from 'vitest' +import request from 'supertest' +import { randomBytes } from 'node:crypto' +import { SIGNED_IN_HEADERS as H, TEST_KEY, TestAppOptions, createTestApp } from '../../testing/test-app' +import { BankProvider, ProviderError } from '../providers/bank-provider' +import { DEFAULT_CONSENT_DAYS } from '../constants' +import { isoDaysFromNow } from '../../shared/constants/time' + +type TestApp = Awaited> +const open: TestApp[] = [] + +async function make(options?: TestAppOptions) { + const t = await createTestApp(options) + + open.push(t) + + return t +} +afterEach(async () => { + while (open.length) { + await open.pop()!.close() + } +}) + +const cookieOf = (res: request.Response) => String(res.headers['set-cookie']?.[0] ?? '').split(';')[0] +const loc = (res: request.Response) => String(res.headers.location ?? '') +const stateOf = (url: string) => new URL(url, 'http://x').searchParams.get('state')! +const idOf = (res: request.Response) => new URL(loc(res), 'http://x').searchParams.get('c')! + +/** connect → test bank page → approve or deny → the bank's return. Resolves to the return response. */ +async function connectBank(t: TestApp, bank = 'N26', extra: Record = {}, approve = true) { + const start = await request(t.server) + .post('/api/banking/connect') + .set(H) + .send({ bank, ...extra }) + .expect(200) + const decide = await request(t.server) + .get( + `/api/banking/mock-bank/decide?state=${encodeURIComponent(stateOf(start.body.url))}&d=${approve ? 'approve' : 'deny'}`, + ) + .expect(302) + + return request(t.server).get(loc(decide)).set('Cookie', cookieOf(start)) +} + +describe('access control (production-style)', () => { + const verify = async (t: string) => { + if (t === 'me') { + return { email: 'me@x.com' } + } + if (t === 'other') { + return { email: 'other@x.com' } + } + throw new Error('bad') + } + const cloudflare = { + CF_ACCESS_TEAM_DOMAIN: 't', + CF_ACCESS_AUD: 'a', + ALLOWED_USER_EMAILS: 'me@x.com,other@x.com', + } + const as = (token?: string) => (token ? { ...H, 'cf-access-jwt-assertion': token } : H) + + it('refuses every protected route without a valid token, and logs it', async () => { + const t = await make({ env: cloudflare, verify }) + + for (const [method, path] of [ + ['get', '/status'], + ['get', '/banks'], + ['post', '/connect'], + ['get', '/connections'], + ['get', '/transactions'], + ['delete', '/transactions'], + ['post', '/connections/x/sync'], + ['delete', '/connections/x'], + ['get', '/admin/overview'], + ['get', '/admin/audit'], + ] as const) { + expect((await request(t.server)[method](`/api/banking${path}`).set(as())).status, `${method} ${path}`).toBe(401) + expect( + (await request(t.server)[method](`/api/banking${path}`).set(as('garbage'))).status, + `${method} ${path} (bad token)`, + ).toBe(401) + } + const refused = t.db.prepare("SELECT COUNT(*) AS n FROM bank_audit_log WHERE action = 'access.refused'").get() as { + n: number + } + + expect(refused.n).toBeGreaterThan(0) + }) + + it('lets listed people in and refuses everyone else, however valid their sign-in', async () => { + const t = await make({ + env: cloudflare, + verify: async x => ({ + email: x === 'stranger' ? 'stranger@x.com' : `${x}@x.com`, + }), + }) + + await request(t.server).get('/api/banking/status').set(as('me')).expect(200) + await request(t.server).get('/api/banking/status').set(as('other')).expect(200) + await request(t.server).get('/api/banking/status').set(as('stranger')).expect(403) + }) + + it('keeps the bank return page public: it is guarded by its request, not by sign-in', async () => { + const t = await make({ env: cloudflare, verify }) + const res = await request(t.server).get('/api/banking/callback?state=nope&code=x') + + expect(res.status).toBe(302) + expect(loc(res)).toContain('bank=error') + }) + + it('does not expose the test bank page in production-style setups', async () => { + const t = await make({ + env: { + ...cloudflare, + NODE_ENV: 'production', + ALLOWED_ORIGIN: 'https://finkit.test', + BANKING_PROVIDER: 'enablebanking', + EB_APP_ID: 'x', + EB_PRIVATE_KEY: 'k', + BANKING_REDIRECT_URL: 'https://a.b/cb', + }, + verify, + }) + + await request(t.server).get('/api/banking/mock-bank?state=x').expect(404) + }) +}) + +describe('banking fails closed', () => { + it('is hidden when switched off', async () => { + const t = await make({ env: { BANKING_PROVIDER: 'off' } }) + + expect((await request(t.server).get('/api/banking/status').expect(200)).body).toEqual({ enabled: false }) + await request(t.server).post('/api/banking/connect').set(H).send({ bank: 'N26' }).expect(404) + await request(t.server).get('/api/banking/connections').expect(404) + }) + it('refuses the test bank in production', async () => { + const t = await make({ + env: { + NODE_ENV: 'production', + ALLOWED_ORIGIN: 'https://finkit.test', + BANKING_PROVIDER: 'mock', + CF_ACCESS_TEAM_DOMAIN: 't', + CF_ACCESS_AUD: 'a', + ALLOWED_USER_EMAILS: 'me@x.com', + }, + verify: async () => ({ email: 'me@x.com' }), + }) + const signedIn = { 'Cf-Access-Jwt-Assertion': 'tok' } + + expect((await request(t.server).get('/api/banking/status').set(signedIn).expect(200)).body).toEqual({ + enabled: false, + }) + await request(t.server).get('/api/banking/connections').set(signedIn).expect(503) + }) + it('refuses to run in production without sign-in settings', async () => { + await expect( + make({ + env: { + NODE_ENV: 'production', + ALLOWED_ORIGIN: 'https://finkit.test', + BANKING_PROVIDER: 'enablebanking', + EB_APP_ID: 'x', + EB_PRIVATE_KEY: 'k', + BANKING_REDIRECT_URL: 'https://a.b/cb', + }, + }), + ).rejects.toThrow(/CF_ACCESS/) + }) +}) + +describe('changing requests', () => { + it('need the custom header, and the right origin when one is set', async () => { + const t = await make({ env: { ALLOWED_ORIGIN: 'https://finkit.example' } }) + const body = { bank: 'N26' } + + await request(t.server).post('/api/banking/connect').set('Content-Type', 'application/json').send(body).expect(403) + await request(t.server) + .post('/api/banking/connect') + .set(H) + .set('Origin', 'https://evil.example') + .send(body) + .expect(403) + await request(t.server) + .post('/api/banking/connect') + .set(H) + .set('Origin', 'https://finkit.example') + .send(body) + .expect(200) + }) +}) + +describe('connect flow with the test bank', () => { + it('connects, lists safely, syncs, throttles and disconnects', async () => { + const t = await make() + const cb = await connectBank(t) + + expect(cb.status).toBe(302) + expect(loc(cb)).toMatch(/^\/app\/banks\?bank=connected&c=[0-9a-f-]{36}$/) + + const list = (await request(t.server).get('/api/banking/connections').expect(200)).body + + expect(list.connections).toHaveLength(1) + const c = list.connections[0] + + expect(c).toMatchObject({ + bank: 'N26', + status: 'active', + accounts: [{ label: '…4821', currency: 'EUR' }], + }) + expect(JSON.stringify(list)).not.toMatch(/sessionId|secret|mock-/) // nothing credential-like reaches the browser + + const sync = (await request(t.server).post(`/api/banking/connections/${c.id}/sync`).set(H).send({}).expect(200)) + .body + + expect(sync.count).toBeGreaterThan(10) + expect(sync.transactions[0]).toHaveProperty('externalId') + // every transaction says where it came from, and the account id matches the one on the card + expect(sync.transactions[0]).toMatchObject({ + bankName: 'N26', + accountLabel: '…4821', + }) + expect(sync.transactions[0].accountRef).toBe(c.accounts[0].id) + expect(c.accounts[0].id).toMatch(/^[0-9a-f]{16}$/) + const again = await request(t.server) + .post(`/api/banking/connections/${c.id}/sync`) + .set(H) + .send({ from: sync.from, to: sync.to }) + + expect(again.status).toBe(429) + expect(again.body.error).toMatch(/wait/i) + + await request(t.server).delete(`/api/banking/connections/${c.id}`).set(H).expect(200) + expect((await request(t.server).get('/api/banking/connections')).body.connections).toHaveLength(0) + }) + + it('returns the same transaction ids on every fetch and continues where the last sync stopped', async () => { + const clock = { now: () => current } + let current = new Date('2026-10-20T12:00:00Z') + const t = await make({ clock }) + const id = idOf(await connectBank(t)) + const first = (await request(t.server).post(`/api/banking/connections/${id}/sync`).set(H).send({})).body + + current = new Date('2026-10-22T12:00:00Z') + const second = (await request(t.server).post(`/api/banking/connections/${id}/sync`).set(H).send({})).body + const firstAmounts = new Map(first.transactions.map((x: any) => [x.externalId, x.amount])) + const overlap = second.transactions.filter((x: any) => firstAmounts.has(x.externalId)) + + expect(overlap.length).toBeGreaterThan(0) + for (const x of overlap) { + expect(firstAmounts.get(x.externalId)).toBe(x.amount) + } + expect(second.from > first.from).toBe(true) // continued, not restarted + }) + + it('keeps no readable secrets or transactions in the connection tables', async () => { + const t = await make() + const id = idOf(await connectBank(t)) + + await request(t.server).post(`/api/banking/connections/${id}/sync`).set(H).send({}) + const dump = JSON.stringify( + ['bank_connections', 'bank_sync_log', 'bank_audit_log', 'bank_auth_states'].map(n => + t.db.prepare(`SELECT * FROM ${n}`).all(), + ), + ) + + expect(dump).not.toMatch(/REWE|Netflix|Salary|Acme|Amazon|Shell/) + expect(dump).not.toMatch(/mock-[0-9a-f]{16}/) // the session id only exists encrypted + expect( + ( + t.db.prepare('SELECT secret_blob FROM bank_connections').get() as { + secret_blob: string + } + ).secret_blob.startsWith('v1.'), + ).toBe(true) + }) + + it('handles a denied approval without creating a connection', async () => { + const t = await make() + + expect(loc(await connectBank(t, 'N26', {}, false))).toBe('/app/banks?bank=error&reason=denied') + expect((await request(t.server).get('/api/banking/connections')).body.connections).toHaveLength(0) + }) + it('rejects unknown banks and malformed requests', async () => { + const t = await make() + + await request(t.server).post('/api/banking/connect').set(H).send({ bank: 'Nope Bank' }).expect(400) + await request(t.server).post('/api/banking/connect').set(H).send({}).expect(400) + await request(t.server).post('/api/banking/connections/x/sync').set(H).send({ from: 'tomorrow' }).expect(400) + }) + + it('reconnecting replaces the old link and keeps the sync position', async () => { + const t = await make() + const first = idOf(await connectBank(t)) + + await request(t.server).post(`/api/banking/connections/${first}/sync`).set(H).send({}) + const lastTo = t.store!.getConnection(first)!.last_sync_to + const second = idOf(await connectBank(t, 'N26', { reconnect: first })) + + expect(t.store!.getConnection(first)!.status).toBe('replaced') + expect(t.store!.getConnection(first)!.secret_blob).toBe('') + expect(t.store!.getConnection(second)!.last_sync_to).toBe(lastTo) + expect((await request(t.server).get('/api/banking/connections')).body.connections.map((c: any) => c.id)).toEqual([ + second, + ]) + }) +}) + +describe('connecting the same bank twice', () => { + it('replaces the existing link instead of adding a duplicate', async () => { + const t = await make() + const first = idOf(await connectBank(t)) + const second = idOf(await connectBank(t)) // no reconnect flag: the server notices the bank is already connected + + expect(second).not.toBe(first) + expect(t.store!.getConnection(first)!.status).toBe('replaced') + expect((await request(t.server).get('/api/banking/connections')).body.connections.map((c: any) => c.id)).toEqual([ + second, + ]) + }) + it('keeps different banks side by side', async () => { + const t = await make() + + await connectBank(t, 'N26') + await connectBank(t, 'Wise') + expect( + (await request(t.server).get('/api/banking/connections')).body.connections + .map((c: { bank: string }) => c.bank) + .toSorted(), + ).toEqual(['N26', 'Wise']) + }) +}) + +describe('the return page cannot be forged', () => { + async function start(t: TestApp) { + const r = await request(t.server).post('/api/banking/connect').set(H).send({ bank: 'N26' }).expect(200) + + return { state: stateOf(r.body.url), cookie: cookieOf(r), res: r } + } + const count = (t: TestApp) => + ( + t.db.prepare('SELECT COUNT(*) n FROM bank_connections').get() as { + n: number + } + ).n + + it('needs the cookie from the browser that started the connection', async () => { + const t = await make() + const { state } = await start(t) + const code = new URL( + loc(await request(t.server).get(`/api/banking/mock-bank/decide?state=${state}&d=approve`)), + 'http://x', + ).searchParams.get('code')! + + expect(loc(await request(t.server).get(`/api/banking/callback?code=${code}&state=${state}`))).toContain( + 'reason=invalid', + ) // no cookie + expect( + loc( + await request(t.server) + .get(`/api/banking/callback?code=${code}&state=${state}`) + .set('Cookie', 'finkit_bank_state=other'), + ), + ).toContain('reason=invalid') + expect(count(t)).toBe(0) + }) + it('works once only', async () => { + const t = await make() + const { state, cookie } = await start(t) + const decide = loc(await request(t.server).get(`/api/banking/mock-bank/decide?state=${state}&d=approve`)) + + expect(loc(await request(t.server).get(decide).set('Cookie', cookie))).toContain('bank=connected') + expect(loc(await request(t.server).get(decide).set('Cookie', cookie))).toMatch(/reason=(expired|invalid)/) + expect(count(t)).toBe(1) + }) + it('expires after ten minutes', async () => { + const t = await make() + const state = randomBytes(8).toString('hex') + + t.store!.createState({ + state, + userId: 'u', + bankName: 'N26', + country: 'DE', + ttlMs: -1000, + }) + expect( + loc( + await request(t.server) + .get(`/api/banking/callback?code=c&state=${state}`) + .set('Cookie', `finkit_bank_state=${state}`), + ), + ).toContain('reason=expired') + }) + it('sets an HttpOnly, SameSite cookie scoped to the banking path, with no caching', async () => { + const t = await make() + const { res } = await start(t) + const c = String(res.headers['set-cookie']) + + expect(c).toMatch(/HttpOnly/i) + expect(c).toMatch(/SameSite=Lax/i) + expect(c).toMatch(/Path=\/api\/banking/) + expect(c).toMatch(/Max-Age=600/) + expect(res.headers['cache-control']).toBe('no-store') + }) + it('is Secure in production', async () => { + const stub: BankProvider = { + name: 'mock', + listBanks: async () => [{ name: 'B', country: 'DE', maxConsentDays: 90 }], + startAuth: async () => ({ url: 'https://bank' }), + completeAuth: async () => { + throw new Error('x') + }, + fetchTransactions: async () => [], + revoke: async () => {}, + } + const t = await make({ + env: { + NODE_ENV: 'production', + ALLOWED_ORIGIN: 'https://finkit.test', + BANKING_PROVIDER: 'enablebanking', + EB_APP_ID: 'x', + EB_PRIVATE_KEY: 'k', + BANKING_REDIRECT_URL: 'https://a.b/cb', + CF_ACCESS_TEAM_DOMAIN: 't', + CF_ACCESS_AUD: 'a', + BANKING_ADMIN_EMAILS: 'a@b.c', + }, + provider: stub, + verify: async () => ({ email: 'a@b.c' }), + }) + const r = await request(t.server) + .post('/api/banking/connect') + .set(H) + .set('cf-access-jwt-assertion', 't') + .set('Origin', 'https://finkit.test') + .send({ bank: 'B' }) + .expect(200) + + expect(String(r.headers['set-cookie'])).toMatch(/Secure/i) + }) +}) + +describe('sync failures', () => { + function failing(kind: 'consent_expired' | 'rate_limited' | 'unavailable'): BankProvider { + return { + name: 'mock', + listBanks: async () => [{ name: 'B', country: 'DE', maxConsentDays: 180 }], + startAuth: async p => ({ url: `/x?state=${p.state}` }), + completeAuth: async () => ({ + sessionId: 's', + accounts: [{ uid: 'u', stableId: 's', label: '…1' }], + validUntil: isoDaysFromNow(DEFAULT_CONSENT_DAYS), + }), + fetchTransactions: async () => { + throw new ProviderError('nope', kind) + }, + revoke: async () => {}, + } + } + async function connected(kind: 'consent_expired' | 'rate_limited' | 'unavailable') { + const t = await make({ provider: failing(kind) }) + const r = await request(t.server).post('/api/banking/connect').set(H).send({ bank: 'B' }).expect(200) + const cb = await request(t.server) + .get(`/api/banking/callback?code=c&state=${stateOf(r.body.url)}`) + .set('Cookie', cookieOf(r)) + + return { t, id: idOf(cb) } + } + const sync = (t: TestApp, id: string) => request(t.server).post(`/api/banking/connections/${id}/sync`).set(H).send({}) + + it('marks the connection expired when the bank says access ended', async () => { + const { t, id } = await connected('consent_expired') + + expect((await sync(t, id)).status).toBe(409) + expect((await request(t.server).get('/api/banking/connections')).body.connections[0].status).toBe('expired') + expect((await sync(t, id)).status).toBe(409) + }) + it('reports rate limiting as 429', async () => { + const { t, id } = await connected('rate_limited') + + expect((await sync(t, id)).status).toBe(429) + }) + it('reports outages without leaking detail, and logs them', async () => { + const { t, id } = await connected('unavailable') + const r = await sync(t, id) + + expect(r.status).toBe(502) + expect(JSON.stringify(r.body)).not.toContain('nope') + expect((await request(t.server).get('/api/banking/admin/sync-log')).body.entries[0]).toMatchObject({ + status: 'failed', + bank: 'B', + }) + }) +}) + +describe('back office (development mode)', () => { + it('reports setup checks, sync stats and an audit trail', async () => { + const t = await make() + const id = idOf(await connectBank(t)) + + await request(t.server).post(`/api/banking/connections/${id}/sync`).set(H).send({}) + const o = (await request(t.server).get('/api/banking/admin/overview').expect(200)).body + + expect(o.connections).toMatchObject({ total: 1, active: 1 }) + expect(o.syncs30d.ok).toBe(1) + expect(o.checks.find((c: any) => c.id === 'access').ok).toBe(false) // development mode is flagged + const actions = (await request(t.server).get('/api/banking/admin/audit')).body.entries.map((e: any) => e.action) + + expect(actions).toEqual(expect.arrayContaining(['connect.start', 'connect.complete', 'sync'])) + }) + it('can age a test connection to show the expiring and expired states', async () => { + const t = await make() + const id = idOf(await connectBank(t)) + + await request(t.server).post(`/api/banking/admin/connections/${id}/expire`).set(H).expect(200) + expect((await request(t.server).get('/api/banking/connections')).body.connections[0].status).toBe('expiring') + await request(t.server).post(`/api/banking/admin/connections/${id}/expire?mode=expired`).set(H).expect(200) + expect((await request(t.server).get('/api/banking/connections')).body.connections[0].status).toBe('expired') + }) +}) + +void TEST_KEY diff --git a/api/src/bank-connections/connections/connection-status.ts b/api/src/bank-connections/connections/connection-status.ts new file mode 100644 index 0000000..56435b9 --- /dev/null +++ b/api/src/bank-connections/connections/connection-status.ts @@ -0,0 +1,14 @@ +import type { ConnectionRow } from './bank-connections.store' +import { MS_PER_DAY } from '../../shared/constants/time' +import { CONNECTION_EXPIRING_SOON_DAYS } from '../constants' + +export { CONNECTION_EXPIRING_SOON_DAYS as EXPIRING_DAYS } + +/** What a person should be told about a connection: it runs out of consent after a set number of days. */ +export function connectionStatus(row: Pick, now: Date) { + const left = Math.ceil((new Date(row.valid_until).getTime() - now.getTime()) / MS_PER_DAY) + const status: 'active' | 'expiring' | 'expired' = + row.status === 'expired' || left <= 0 ? 'expired' : left <= CONNECTION_EXPIRING_SOON_DAYS ? 'expiring' : 'active' + + return { status, daysLeft: Math.max(left, 0) } +} diff --git a/api/src/bank-connections/connections/rate-limiter.ts b/api/src/bank-connections/connections/rate-limiter.ts new file mode 100644 index 0000000..a586fd8 --- /dev/null +++ b/api/src/bank-connections/connections/rate-limiter.ts @@ -0,0 +1,22 @@ +/** Small in-memory limiter (this is a single-instance personal app). */ +export class RateLimiter { + private readonly hits = new Map() + + constructor(private readonly now: () => number) {} + + /** Returns false when `key` has already used up `max` hits in the last `windowMs`. */ + allow(key: string, windowMs: number, max: number): boolean { + const t = this.now() + const recent = (this.hits.get(key) || []).filter(x => t - x < windowMs) + + if (recent.length >= max) { + this.hits.set(key, recent) + + return false + } + recent.push(t) + this.hits.set(key, recent) + + return true + } +} diff --git a/api/src/bank-connections/constants/connections.ts b/api/src/bank-connections/constants/connections.ts new file mode 100644 index 0000000..866c4e1 --- /dev/null +++ b/api/src/bank-connections/constants/connections.ts @@ -0,0 +1,17 @@ +import { MS_PER_HOUR, MS_PER_MINUTE } from '../../shared/constants/time' + +/** Days before `valid_until` when we show “expiring soon”. */ +export const CONNECTION_EXPIRING_SOON_DAYS = 14 + +/** Bank list cache for connect flow. */ +export const BANK_LIST_CACHE_TTL_MS = MS_PER_HOUR + +/** Per-email connect attempts. */ +export const CONNECT_RATE_LIMIT_WINDOW_MS = 10 * MS_PER_MINUTE +export const CONNECT_RATE_LIMIT_MAX_ATTEMPTS = 10 + +/** OAuth `state` cookie and DB row lifetime. */ +export const OAUTH_STATE_TTL_MS = 10 * MS_PER_MINUTE + +export const SYNC_LOG_RETENTION_DAYS = 90 +export const AUDIT_LOG_RETENTION_DAYS = 365 diff --git a/api/src/bank-connections/constants/enable-banking.ts b/api/src/bank-connections/constants/enable-banking.ts new file mode 100644 index 0000000..f49c85a --- /dev/null +++ b/api/src/bank-connections/constants/enable-banking.ts @@ -0,0 +1,22 @@ +import { SECONDS_PER_DAY, SECONDS_PER_HOUR } from '../../shared/constants/time' + +/** Default PSU consent length when Enable Banking omits `maximum_consent_validity` (seconds). */ +export const DEFAULT_CONSENT_VALIDITY_SECONDS = 90 * SECONDS_PER_DAY + +export const DEFAULT_CONSENT_DAYS = DEFAULT_CONSENT_VALIDITY_SECONDS / SECONDS_PER_DAY + +/** RS256 JWT lifetime for Enable Banking API calls. */ +export const ENABLE_BANKING_JWT_TTL_SECONDS = SECONDS_PER_HOUR + +/** Refresh cached JWT this many seconds before `exp`. */ +export const ENABLE_BANKING_JWT_REFRESH_BUFFER_SECONDS = 60 + +/** Far above a normal pagination run; hitting this fails instead of truncating silently. */ +export const ENABLE_BANKING_MAX_TRANSACTION_PAGES = 500 + +export const ENABLE_BANKING_REQUEST_TIMEOUT_MS = 30_000 + +/** Mock bank uses a longer fake consent for local testing. */ +export const TEST_BANK_DEFAULT_CONSENT_DAYS = 179 + +export const TEST_BANK_MAX_MOCK_HISTORY_DAYS = 400 diff --git a/api/src/bank-connections/constants/index.ts b/api/src/bank-connections/constants/index.ts new file mode 100644 index 0000000..30872d8 --- /dev/null +++ b/api/src/bank-connections/constants/index.ts @@ -0,0 +1,3 @@ +export * from './connections' +export * from './enable-banking' +export * from './sync' diff --git a/api/src/bank-connections/constants/sync.ts b/api/src/bank-connections/constants/sync.ts new file mode 100644 index 0000000..e2aafc2 --- /dev/null +++ b/api/src/bank-connections/constants/sync.ts @@ -0,0 +1,15 @@ +import { MS_PER_HOUR, MS_PER_MINUTE } from '../../shared/constants/time' + +export const MIN_SYNC_INTERVAL_MS = MS_PER_MINUTE +export const MAX_SYNC_RANGE_DAYS = 366 +/** Re-fetch a few days already seen; repeats are skipped by the bank's own ids. */ +export const SYNC_OVERLAP_DAYS = 3 +export const DEFAULT_SYNC_HISTORY_DAYS = 90 + +/** Poll interval for scheduled catch-up sync (independent of `BANKING_AUTO_SYNC_HOURS`). */ +export const AUTO_SYNC_CHECK_INTERVAL_MS = 30 * MS_PER_MINUTE + +/** After the app starts, catch up once soon: it may have been stopped for days (scheduled hosting). */ +export const AUTO_SYNC_STARTUP_DELAY_MS = MS_PER_MINUTE + +export const AUTO_SYNC_LOOKBACK_MS = (hours: number) => hours * MS_PER_HOUR diff --git a/api/src/bank-connections/providers/bank-provider.ts b/api/src/bank-connections/providers/bank-provider.ts new file mode 100644 index 0000000..0ee4e52 --- /dev/null +++ b/api/src/bank-connections/providers/bank-provider.ts @@ -0,0 +1,83 @@ +// The contract every bank provider (the real one and the test bank) fulfils. + +// What the rest of the API knows about bank providers. Real and mock providers both fit this. + +export interface BankInfo { + name: string + country: string + maxConsentDays: number + /** Headers the bank wants (all or none) to treat a request as made by a user who is present */ + requiredPsuHeaders?: string[] +} + +export interface ProviderAccount { + uid: string // provider id, only valid for the life of a session + stableId: string // survives reconnects (used to recognise the same transaction again) + label: string // masked, safe to show: "…1234" + currency?: string + /** The account's own IBAN, so transfers between the user's accounts can be recognised. Stored encrypted. */ + iban?: string +} + +export interface ProviderSession { + sessionId: string + accounts: ProviderAccount[] + validUntil: string // ISO + /** Kept from connect time: what this bank needs to see to treat a request as user-initiated */ + requiredPsuHeaders?: string[] +} + +/** Who is pressing Sync, as the bank's fraud checks like to know. Never stored. */ +export interface PsuInfo { + ip?: string + userAgent?: string +} + +// Shaped like the web app's RawTransaction, so a sync feeds the normal import path +export interface BankTransaction { + date: string // YYYY-MM-DD + description: string + amount: number // negative = money out + currency: string + recipient?: string + recipientIban?: string + externalId?: string + accountId?: string + /** The user's own account number (IBAN), when the bank gives it */ + referenceAccount?: string + // Where it came from, so the app can browse bank → account → transactions + bankName?: string + accountLabel?: string + accountRef?: string + rawData: Record +} + +export class ProviderError extends Error { + constructor( + message: string, + public readonly kind: 'consent_expired' | 'rate_limited' | 'unavailable' | 'rejected' | 'too_large', + public readonly status?: number, + ) { + super(message) + } +} + +export interface BankProvider { + readonly name: 'mock' | 'enablebanking' + listBanks(country: string): Promise + startAuth(p: { bank: BankInfo; redirectUrl: string; state: string; validUntil: Date }): Promise<{ url: string }> + completeAuth(code: string): Promise + fetchTransactions(p: { + session: ProviderSession + account: ProviderAccount + from: string + to: string + /** Present only when a person asked for this fetch (not for scheduled ones) */ + psu?: PsuInfo + /** Ask the bank for the longest history it allows (ignores from/to), for the very first fetch */ + longest?: boolean + }): Promise + revoke(sessionId: string): Promise +} + +export const BANK_PROVIDER = Symbol('BANK_PROVIDER') diff --git a/api/src/bank-connections/providers/enable-banking.provider.test.ts b/api/src/bank-connections/providers/enable-banking.provider.test.ts new file mode 100644 index 0000000..b132db7 --- /dev/null +++ b/api/src/bank-connections/providers/enable-banking.provider.test.ts @@ -0,0 +1,367 @@ +import { describe, it, expect } from 'vitest' +import { createVerify, generateKeyPairSync } from 'node:crypto' +import { ProviderError } from './bank-provider' +import { EnableBankingProvider, mapEbTransaction, signJwt } from './enable-banking.provider' +import { ENABLE_BANKING_JWT_TTL_SECONDS } from '../constants' + +describe('mapper', () => { + const tx = (o: object) => + ({ + transaction_amount: { amount: '12.50', currency: 'EUR' }, + credit_debit_indicator: 'DBIT', + status: 'BOOK', + booking_date: '2026-10-05', + ...o, + }) as never + + it('makes debits negative and uses the creditor as recipient', () => { + const m = mapEbTransaction( + tx({ + creditor: { name: 'REWE' }, + remittance_information: ['Einkauf', '123'], + entry_reference: 'r1', + }), + 'acc', + )! + + expect(m).toMatchObject({ + amount: -12.5, + recipient: 'REWE', + description: 'Einkauf 123', + externalId: 'acc:r1', + date: '2026-10-05', + }) + }) + it('keeps credits positive and uses the debtor', () => { + expect(mapEbTransaction(tx({ credit_debit_indicator: 'CRDT', debtor: { name: 'Acme' } }), 'a')).toMatchObject({ + amount: 12.5, + recipient: 'Acme', + }) + }) + it('skips pending transactions and rows without a date or amount', () => { + expect(mapEbTransaction(tx({ status: 'PDNG' }), 'a')).toBeNull() + expect(mapEbTransaction(tx({ booking_date: null }), 'a')).toBeNull() + expect(mapEbTransaction(tx({ transaction_amount: { amount: 'x', currency: 'EUR' } }), 'a')).toBeNull() + }) + + describe('bank-to-bank differences', () => { + it('card payment: no counterparty, merchant only in the remittance text', () => { + expect( + mapEbTransaction( + tx({ remittance_information: ['KARTENZAHLUNG REWE SAGT DANKE 4711', null, ''], transaction_id: 't9' }), + 'a', + ), + ).toMatchObject({ + description: 'KARTENZAHLUNG REWE SAGT DANKE 4711', + recipient: undefined, + externalId: 'a:t9', + }) + }) + it('falls back to note, then counterparty name, then empty description', () => { + expect(mapEbTransaction(tx({ note: 'Miete Okt', creditor: { name: 'Vermieter' } }), 'a')!.description).toBe( + 'Miete Okt', + ) + expect(mapEbTransaction(tx({ creditor: { name: 'Vermieter' } }), 'a')!.description).toBe('Vermieter') + expect(mapEbTransaction(tx({}), 'a')).toMatchObject({ description: '', recipient: undefined }) + }) + it('uses value or transaction date when booking date is missing, and trims timestamps', () => { + expect(mapEbTransaction(tx({ booking_date: null, value_date: '2026-10-07' }), 'a')!.date).toBe('2026-10-07') + expect(mapEbTransaction(tx({ booking_date: '2026-10-06T00:00:00Z' }), 'a')!.date).toBe('2026-10-06') + }) + it('signs by indicator even when the bank sends a signed amount', () => { + const neg = { amount: '-12.50', currency: 'EUR' } + + expect(mapEbTransaction(tx({ transaction_amount: neg }), 'a')!.amount).toBe(-12.5) + expect(mapEbTransaction(tx({ transaction_amount: neg, credit_debit_indicator: 'CRDT' }), 'a')!.amount).toBe(12.5) + }) + it('has no externalId when the bank gives no reference at all', () => { + expect(mapEbTransaction(tx({}), 'a')!.externalId).toBeUndefined() + }) + it('takes the counterparty IBAN from the right side', () => { + expect( + mapEbTransaction(tx({ creditor_account: { iban: 'DE01' }, debtor_account: { iban: 'DE02' } }), 'a')! + .recipientIban, + ).toBe('DE01') + }) + it('keeps non-EUR currencies', () => { + expect(mapEbTransaction(tx({ transaction_amount: { amount: '5', currency: 'USD' } }), 'a')!.currency).toBe('USD') + }) + }) +}) + +describe('Enable Banking client (fake server)', () => { + const { privateKey, publicKey } = generateKeyPairSync('rsa', { + modulusLength: 2048, + }) + const pem = privateKey.export({ type: 'pkcs8', format: 'pem' }).toString() + + it('signs RS256 JWTs the API will accept', () => { + const t = signJwt('app-123', pem, 1000, ENABLE_BANKING_JWT_TTL_SECONDS) + const [h, p, s] = t.split('.') + + expect(JSON.parse(Buffer.from(h, 'base64url').toString())).toEqual({ + typ: 'JWT', + alg: 'RS256', + kid: 'app-123', + }) + expect(JSON.parse(Buffer.from(p, 'base64url').toString())).toEqual({ + iss: 'enablebanking.com', + aud: 'api.enablebanking.com', + iat: 1000, + exp: 4600, + }) + expect(createVerify('RSA-SHA256').update(`${h}.${p}`).verify(publicKey, Buffer.from(s, 'base64url'))).toBe(true) + }) + + function fake( + handler: (req: { + method: string + path: string + body: any + auth: string | null + headers: Record + }) => { + status?: number + body?: unknown + }, + ) { + const calls: Array<{ + method: string + path: string + body: any + auth: string | null + headers: Record + }> = [] + const fetchImpl = (async (url: string, init: RequestInit) => { + const u = new URL(url) + const req = { + method: init.method || 'GET', + path: u.pathname + u.search, + body: init.body ? JSON.parse(String(init.body)) : undefined, + auth: (init.headers as Record).Authorization || null, + headers: init.headers as Record, + } + + calls.push(req) + const r = handler(req) + + return new Response(JSON.stringify(r.body ?? {}), { + status: r.status ?? 200, + }) + }) as unknown as typeof fetch + + return { + provider: new EnableBankingProvider({ + appId: 'app', + privateKey: pem, + fetchImpl, + }), + calls, + } + } + + it('lists personal-account banks with their consent length', async () => { + const { provider, calls } = fake(() => ({ + body: { + aspsps: [ + { + name: 'N26', + country: 'DE', + psu_types: ['business', 'personal'], + maximum_consent_validity: 15552000, + }, + { name: 'BizOnly', country: 'DE', psu_types: ['business'] }, + ], + }, + })) + + expect(await provider.listBanks('de')).toEqual([{ name: 'N26', country: 'DE', maxConsentDays: 180 }]) + expect(calls[0].path).toBe('/aspsps?country=DE') + }) + it('starts and completes authorisation', async () => { + const { provider, calls } = fake(r => + r.path === '/auth' + ? { body: { url: 'https://bank/login' } } + : { + body: { + session_id: 'S1', + accounts: [ + { + uid: 'u1', + identification_hash: 'h1', + account_id: { iban: 'DE001234' }, + currency: 'EUR', + }, + ], + access: { valid_until: '2027-01-01T00:00:00Z' }, + }, + }, + ) + const { url } = await provider.startAuth({ + bank: { name: 'N26', country: 'DE', maxConsentDays: 180 }, + redirectUrl: 'https://x/cb', + state: 'st', + validUntil: new Date('2027-01-01'), + }) + + expect(url).toBe('https://bank/login') + expect(calls[0].body).toMatchObject({ + aspsp: { name: 'N26', country: 'DE' }, + state: 'st', + redirect_url: 'https://x/cb', + psu_type: 'personal', + }) + expect(calls[0]).toBeDefined() + const s = await provider.completeAuth('code1') + + expect(calls[1].body).toEqual({ code: 'code1' }) + expect(s).toMatchObject({ + sessionId: 'S1', + validUntil: '2027-01-01T00:00:00Z', + accounts: [{ uid: 'u1', stableId: 'h1', label: '…1234', currency: 'EUR', iban: 'DE001234' }], + }) + }) + describe('PSU headers and the longest history', () => { + const account = { uid: 'u1', stableId: 'h1', label: '' } + const empty = () => ({ body: { transactions: [] } }) + const run = ( + provider: ReturnType['provider'], + extra: Record = {}, + required?: string[], + ) => + provider.fetchTransactions({ + session: { sessionId: 'S', accounts: [], validUntil: '', requiredPsuHeaders: required }, + account, + from: '2026-10-01', + to: '2026-10-31', + ...extra, + } as never) + + it('sends the person’s IP and browser when the bank asks for exactly those', async () => { + const { provider, calls } = fake(empty) + + await run(provider, { psu: { ip: '203.0.113.7', userAgent: 'Mozilla/5.0' } }, [ + 'Psu-Ip-Address', + 'Psu-User-Agent', + ]) + expect(calls[0].headers).toMatchObject({ 'Psu-Ip-Address': '203.0.113.7', 'Psu-User-Agent': 'Mozilla/5.0' }) + }) + it('sends none when the bank wants something we cannot honestly supply', async () => { + const { provider, calls } = fake(empty) + + await run(provider, { psu: { ip: '203.0.113.7', userAgent: 'x' } }, ['Psu-Ip-Address', 'Psu-Geo-Location']) + expect(Object.keys(calls[0].headers).filter(h => h.startsWith('Psu'))).toEqual([]) + }) + it('sends none for scheduled fetches, or when the bank asks for none', async () => { + const { provider, calls } = fake(empty) + + await run(provider, {}, ['Psu-Ip-Address']) + await run(provider, { psu: { ip: '203.0.113.7' } }) + expect(calls.every(c => !Object.keys(c.headers).some(h => h.startsWith('Psu')))).toBe(true) + }) + it('asks for the longest history without dates, and falls back to the window if refused', async () => { + const ok = fake(empty) + + await run(ok.provider, { longest: true }) + expect(ok.calls[0].path).toContain('strategy=longest') + expect(ok.calls[0].path).not.toContain('date_from') + + const refuse = fake(r => (r.path.includes('strategy=longest') ? { status: 400 } : empty())) + + await run(refuse.provider, { longest: true }) + expect(refuse.calls).toHaveLength(2) + expect(refuse.calls[1].path).toContain('date_from=2026-10-01') + }) + it('does not fall back when the bank is limiting us or the consent is gone', async () => { + const { provider, calls } = fake(() => ({ status: 429 })) + + await expect(run(provider, { longest: true })).rejects.toMatchObject({ kind: 'rate_limited' }) + expect(calls).toHaveLength(1) + }) + }) + it('fails instead of quietly cutting the list short when the bank never stops paging', async () => { + const { provider } = fake(() => ({ + body: { + continuation_key: 'more', + transactions: [], + }, + })) + + await expect( + provider.fetchTransactions({ + session: { sessionId: 'S', accounts: [], validUntil: '' }, + account: { uid: 'u1', stableId: 'h1', label: '' }, + from: '2020-01-01', + to: '2026-10-31', + }), + ).rejects.toMatchObject({ kind: 'too_large' }) + }) + it('follows continuation keys and skips pending rows', async () => { + const { provider, calls } = fake(r => + r.path.includes('continuation_key=k2') + ? { + body: { + transactions: [ + { + transaction_amount: { amount: '1', currency: 'EUR' }, + credit_debit_indicator: 'DBIT', + status: 'BOOK', + booking_date: '2026-10-02', + entry_reference: 'b', + }, + ], + }, + } + : { + body: { + continuation_key: 'k2', + transactions: [ + { + transaction_amount: { amount: '2', currency: 'EUR' }, + credit_debit_indicator: 'DBIT', + status: 'BOOK', + booking_date: '2026-10-01', + entry_reference: 'a', + }, + { + transaction_amount: { amount: '3', currency: 'EUR' }, + credit_debit_indicator: 'DBIT', + status: 'PDNG', + booking_date: '2026-10-01', + }, + ], + }, + }, + ) + const out = await provider.fetchTransactions({ + session: { sessionId: 'S', accounts: [], validUntil: '' }, + account: { uid: 'u1', stableId: 'h1', label: '', iban: 'DE001234' }, + from: '2026-10-01', + to: '2026-10-31', + }) + + expect(out.map(t => t.externalId)).toEqual(['h1:a', 'h1:b']) + expect(out.every(t => t.referenceAccount === 'DE001234')).toBe(true) + expect(calls).toHaveLength(2) + expect(calls[0].path).toContain('date_from=2026-10-01') + expect(calls[0].path).toContain('date_to=2026-10-31') + }) + it('sends the signed token and maps failures to clear kinds', async () => { + for (const [status, kind] of [ + [401, 'consent_expired'], + [429, 'rate_limited'], + [503, 'unavailable'], + [400, 'rejected'], + ] as const) { + const { provider, calls } = fake(() => ({ + status, + body: { secret: 'must-not-leak' }, + })) + const err = (await provider.listBanks('DE').catch(e => e)) as ProviderError + + expect(err.kind).toBe(kind) + expect(err.message).not.toContain('must-not-leak') + expect(calls[0].auth).toMatch(/^Bearer [\w-]+\.[\w-]+\.[\w-]+$/) + } + }) +}) diff --git a/api/src/bank-connections/providers/enable-banking.provider.ts b/api/src/bank-connections/providers/enable-banking.provider.ts new file mode 100644 index 0000000..7b7abc4 --- /dev/null +++ b/api/src/bank-connections/providers/enable-banking.provider.ts @@ -0,0 +1,325 @@ +import { createSign } from 'node:crypto' +import { + DEFAULT_CONSENT_DAYS, + DEFAULT_CONSENT_VALIDITY_SECONDS, + ENABLE_BANKING_JWT_REFRESH_BUFFER_SECONDS, + ENABLE_BANKING_JWT_TTL_SECONDS, + ENABLE_BANKING_MAX_TRANSACTION_PAGES, + ENABLE_BANKING_REQUEST_TIMEOUT_MS, +} from '../constants' +import { isoDaysFromNow, SECONDS_PER_DAY } from '../../shared/constants/time' +import { + BankInfo, + BankProvider, + BankTransaction, + ProviderAccount, + ProviderError, + ProviderSession, + PsuInfo, +} from './bank-provider' +// Shape of an Enable Banking transaction (only the fields we use) +interface EbTransaction { + entry_reference?: string | null + transaction_id?: string | null + transaction_amount: { amount: string; currency: string } + credit_debit_indicator: 'CRDT' | 'DBIT' + status?: string + booking_date?: string | null + value_date?: string | null + transaction_date?: string | null + creditor?: { name?: string | null } | null + debtor?: { name?: string | null } | null + creditor_account?: { iban?: string | null } | null + debtor_account?: { iban?: string | null } | null + remittance_information?: string[] | null + note?: string | null +} + +/** Booked transactions only: pending ones can change or disappear. */ +export function mapEbTransaction(tx: EbTransaction, accountStableId: string): BankTransaction | null { + if (tx.status && tx.status !== 'BOOK') { + return null + } + const date = tx.booking_date || tx.value_date || tx.transaction_date + const raw = Number(tx.transaction_amount?.amount) + + if (!date || !Number.isFinite(raw)) { + return null + } + + const debit = tx.credit_debit_indicator === 'DBIT' + const counterparty = debit ? tx.creditor : tx.debtor + const counterpartyAccount = debit ? tx.creditor_account : tx.debtor_account + const remittance = (tx.remittance_information || []).filter(Boolean).join(' ').trim() + const ref = tx.entry_reference || tx.transaction_id || undefined + + return { + date: date.slice(0, 10), + description: remittance || tx.note || counterparty?.name || '', + amount: debit ? -Math.abs(raw) : Math.abs(raw), + currency: tx.transaction_amount.currency, + recipient: counterparty?.name || undefined, + recipientIban: counterpartyAccount?.iban || undefined, + // Stable across syncs AND reconnects, so re-fetching the same day never duplicates + externalId: ref ? `${accountStableId}:${ref}` : undefined, + accountId: accountStableId, + rawData: { source: 'enablebanking' }, + } +} + +// ---------------- API client ---------------- +// Client for the Enable Banking API (https://enablebanking.com/docs/api/reference/). +// Requests are signed with the application's RSA private key (RS256 JWT). The key never leaves +// this process, is never logged, and is never sent to the browser. +// +// Built from their documentation. Exercised in tests against a fake server, but not yet against +// the live API. + +const BASE_URL = 'https://api.enablebanking.com' + +const b64url = (v: Buffer | string) => Buffer.from(v).toString('base64url') + +export function signJwt( + appId: string, + privateKeyPem: string, + nowSeconds = Math.floor(Date.now() / 1000), + ttlSeconds = ENABLE_BANKING_JWT_TTL_SECONDS, +): string { + const header = { typ: 'JWT', alg: 'RS256', kid: appId } + const payload = { + iss: 'enablebanking.com', + aud: 'api.enablebanking.com', + iat: nowSeconds, + exp: nowSeconds + ttlSeconds, + } + const unsigned = `${b64url(JSON.stringify(header))}.${b64url(JSON.stringify(payload))}` + const signature = createSign('RSA-SHA256').update(unsigned).sign(privateKeyPem) + + return `${unsigned}.${b64url(signature)}` +} + +interface EbAccount { + uid: string + identification_hash?: string + account_id?: { iban?: string | null } | null + currency?: string | null + name?: string | null +} + +export class EnableBankingProvider implements BankProvider { + readonly name = 'enablebanking' as const + private cachedJwt: { token: string; expires: number } | null = null + + constructor( + private readonly opts: { + appId: string + privateKey: string + baseUrl?: string + fetchImpl?: typeof fetch + }, + ) {} + + private jwt(): string { + const now = Math.floor(Date.now() / 1000) + + if (this.cachedJwt && this.cachedJwt.expires - ENABLE_BANKING_JWT_REFRESH_BUFFER_SECONDS > now) { + return this.cachedJwt.token + } + const token = signJwt(this.opts.appId, this.opts.privateKey, now, ENABLE_BANKING_JWT_TTL_SECONDS) + + this.cachedJwt = { token, expires: now + ENABLE_BANKING_JWT_TTL_SECONDS } + + return token + } + + private async api( + method: string, + path: string, + body?: unknown, + extraHeaders: Record = {}, + ): Promise { + const doFetch = this.opts.fetchImpl ?? fetch + let res: Response + + try { + res = await doFetch(`${this.opts.baseUrl ?? BASE_URL}${path}`, { + method, + headers: { + Authorization: `Bearer ${this.jwt()}`, + ...extraHeaders, + ...(body ? { 'Content-Type': 'application/json' } : {}), + }, + body: body ? JSON.stringify(body) : undefined, + signal: AbortSignal.timeout(ENABLE_BANKING_REQUEST_TIMEOUT_MS), + }) + } catch { + throw new ProviderError('Could not reach the bank service', 'unavailable') + } + if (res.ok) { + return (res.status === 204 ? undefined : await res.json()) as T + } + // Keep error detail short and free of anything sensitive + const kind = + res.status === 429 + ? 'rate_limited' + : res.status === 401 || res.status === 403 || res.status === 404 + ? 'consent_expired' + : res.status >= 500 + ? 'unavailable' + : 'rejected' + + throw new ProviderError(`Bank service returned ${res.status}`, kind, res.status) + } + + async listBanks(country: string): Promise { + const data = await this.api<{ + aspsps: Array<{ + name: string + country: string + psu_types?: string[] + maximum_consent_validity?: number + required_psu_headers?: string[] + }> + }>('GET', `/aspsps?country=${encodeURIComponent(country.toUpperCase())}`) + + return (data.aspsps || []) + .filter(a => !a.psu_types || a.psu_types.includes('personal')) + .map(a => ({ + name: a.name, + country: a.country, + maxConsentDays: Math.floor((a.maximum_consent_validity ?? DEFAULT_CONSENT_VALIDITY_SECONDS) / SECONDS_PER_DAY), + requiredPsuHeaders: a.required_psu_headers?.length ? a.required_psu_headers : undefined, + })) + .toSorted((a, b) => a.name.localeCompare(b.name)) + } + + async startAuth(p: { bank: BankInfo; redirectUrl: string; state: string; validUntil: Date }) { + const data = await this.api<{ url: string }>('POST', '/auth', { + access: { valid_until: p.validUntil.toISOString() }, + aspsp: { name: p.bank.name, country: p.bank.country }, + state: p.state, + redirect_url: p.redirectUrl, + psu_type: 'personal', + }) + + return { url: data.url } + } + + async completeAuth(code: string): Promise { + const data = await this.api<{ + session_id: string + accounts: EbAccount[] + access?: { valid_until?: string } + }>('POST', '/sessions', { code }) + const accounts: ProviderAccount[] = (data.accounts || []).map(a => { + const iban = a.account_id?.iban || '' + + return { + uid: a.uid, + stableId: a.identification_hash || iban || a.uid, + label: iban ? `…${iban.slice(-4)}` : a.name || 'Account', + currency: a.currency || undefined, + iban: iban || undefined, + } + }) + + return { + sessionId: data.session_id, + accounts, + validUntil: data.access?.valid_until || isoDaysFromNow(DEFAULT_CONSENT_DAYS), + } + } + + async fetchTransactions(p: { + session: ProviderSession + account: ProviderAccount + from: string + to: string + psu?: PsuInfo + longest?: boolean + }): Promise { + const headers = psuHeaders(p.session.requiredPsuHeaders, p.psu) + const window = () => new URLSearchParams({ date_from: p.from, date_to: p.to }) + + if (!p.longest) { + return this.collect(p.account, window(), headers) + } + try { + return await this.collect(p.account, new URLSearchParams({ strategy: 'longest' }), headers) + } catch (e) { + // Not every bank can do this: fall back to the normal window + if (e instanceof ProviderError && e.kind === 'rejected') { + return this.collect(p.account, window(), headers) + } + throw e + } + } + + /** Follows the bank's pages until it says there are no more. */ + private async collect( + account: ProviderAccount, + base: URLSearchParams, + headers: Record, + ): Promise { + const out: BankTransaction[] = [] + let key: string | undefined + + for (let page = 0; page < ENABLE_BANKING_MAX_TRANSACTION_PAGES; page++) { + const qs = new URLSearchParams(base) + + if (key) { + qs.set('continuation_key', key) + } + const data = await this.api<{ + transactions?: EbTransaction[] + continuation_key?: string | null + }>('GET', `/accounts/${encodeURIComponent(account.uid)}/transactions?${qs}`, undefined, headers) + + for (const tx of data.transactions || []) { + const mapped = mapEbTransaction(tx, account.stableId) + + if (mapped) { + mapped.referenceAccount = account.iban + out.push(mapped) + } + } + key = data.continuation_key || undefined + if (!key) { + return out + } + } + + throw new ProviderError('Too many transactions in this date range', 'too_large') + } + + async revoke(sessionId: string): Promise { + try { + await this.api('DELETE', `/sessions/${encodeURIComponent(sessionId)}`) + } catch { + /* best effort */ + } + } +} + +/** + * Banks list the headers that tell them "a person is here" (all or none, or the bank answers 422). + * We can honestly supply the person's IP address and browser; if the bank wants anything else, or + * nobody asked for this fetch, send none and the request counts as a background one. + */ +export function psuHeaders(required: string[] | undefined, psu: PsuInfo | undefined): Record { + if (!psu || !required?.length) { + return {} + } + const have: Record = { 'psu-ip-address': psu.ip, 'psu-user-agent': psu.userAgent } + + if (!required.every(name => have[name.toLowerCase()])) { + return {} + } + + return Object.fromEntries( + [ + ['Psu-Ip-Address', psu.ip], + ['Psu-User-Agent', psu.userAgent], + ].filter((entry): entry is [string, string] => !!entry[1]), + ) +} diff --git a/api/src/bank-connections/providers/test-bank.controller.ts b/api/src/bank-connections/providers/test-bank.controller.ts new file mode 100644 index 0000000..7cac97a --- /dev/null +++ b/api/src/bank-connections/providers/test-bank.controller.ts @@ -0,0 +1,57 @@ +import { HttpCode, Controller, Get, Inject, Param, Post, Query, Res, UseGuards, UseInterceptors } from '@nestjs/common' +import type { Response } from 'express' +import { AccessGuard, CurrentActor } from '../../access/access.guard' +import { Actor } from '../../access/access.settings' +import { CsrfGuard } from '../../access/csrf.guard' +import { NoStoreInterceptor } from '../../shared/http/no-store.interceptor' +import { BANK_PROVIDER } from './bank-provider' +import { BankConnectionsService } from '../connections/bank-connections.service' +import { TestBankProvider } from './test-bank.provider' + +const esc = (s: string) => + s.replace(/[&<>"']/g, ch => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[ch]!) + +/** + * Development only (never registered in production): a stand-in for the bank's own approval + * page, plus a way to age a connection to see the "expiring" and "expired" states. + */ +@Controller('api/banking') +@UseInterceptors(NoStoreInterceptor) +export class TestBankController { + constructor( + @Inject(BANK_PROVIDER) private readonly bank: TestBankProvider, + private readonly connections: BankConnectionsService, + ) {} + + @Get('mock-bank') + page(@Query('state') state = '', @Res() res: Response) { + const req = this.bank.pendingFor(state) + + if (!req) { + return res.status(400).send('This test request has expired. Start again from FinKit.') + } + const q = encodeURIComponent(state) + + return res.type('html') + .send(`Test bank +
+TEST BANK · ${esc(req.bank.name)} · not a real bank

Allow FinKit to read your account?

+

This stands in for your bank's own approval page while FinKit is in development.

+
  • Account details (name, last 4 digits)
  • Balances
  • Transactions
  • Cannot make payments or transfers
+ApproveDeny
`) + } + + @Get('mock-bank/decide') + decide(@Query('state') state = '', @Query('d') decision: string, @Res() res: Response) { + const target = this.bank.decide(state, decision === 'approve') + + return target ? res.redirect(302, target) : res.status(400).send('This test request has expired.') + } + + @Post('admin/connections/:id/expire') + @HttpCode(200) // the previous API answered 200, not 201 + @UseGuards(AccessGuard, CsrfGuard) + expire(@CurrentActor() actor: Actor, @Param('id') id: string, @Query('mode') mode?: string) { + return this.connections.ageConnection(actor, id, mode === 'expired' ? 'expired' : 'expiring') + } +} diff --git a/api/src/bank-connections/providers/test-bank.provider.ts b/api/src/bank-connections/providers/test-bank.provider.ts new file mode 100644 index 0000000..dbda3fb --- /dev/null +++ b/api/src/bank-connections/providers/test-bank.provider.ts @@ -0,0 +1,199 @@ +import { Injectable } from '@nestjs/common' +import { randomBytes } from 'node:crypto' +import { MS_PER_DAY, isoDaysFromNow } from '../../shared/constants/time' +import { TEST_BANK_DEFAULT_CONSENT_DAYS, TEST_BANK_MAX_MOCK_HISTORY_DAYS } from '../constants' +import { + BankInfo, + BankProvider, + BankTransaction, + ProviderAccount, + ProviderError, + ProviderSession, +} from './bank-provider' + +// The test bank: a fake bank for development and tests. It has its own approval page (served by the banking +// routes), deterministic transactions, and a few "bad" banks to exercise error states. +// It is refused at startup in production (see config.ts). + +const TEST_BANKS: BankInfo[] = [ + { name: 'N26', country: 'DE', maxConsentDays: 180 }, + { name: 'Wise', country: 'DE', maxConsentDays: 180 }, + { name: 'Revolut', country: 'DE', maxConsentDays: 180 }, + { name: 'Test Bank (always denies)', country: 'DE', maxConsentDays: 180 }, + { name: 'Test Bank (sync fails)', country: 'DE', maxConsentDays: 180 }, +] + +// Small seeded PRNG so the same account and day always yield the same transactions +function mulberry32(seed: number) { + return () => { + seed |= 0 + seed = (seed + 0x6d2b79f5) | 0 + let t = Math.imul(seed ^ (seed >>> 15), 1 | seed) + + t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t + + return ((t ^ (t >>> 14)) >>> 0) / 4294967296 + } +} +function hash(s: string): number { + let h = 2166136261 + + for (let i = 0; i < s.length; i++) { + h ^= s.charCodeAt(i) + h = Math.imul(h, 16777619) + } + + return h >>> 0 +} + +const SPEND: Array<[string, string, number, number]> = [ + ['REWE Markt', 'REWE Einkauf', 12, 70], + ['Shell', 'Fuel', 30, 60], + ['Trainline', 'Trainline.com ticket', 20, 90], + ['Amazon', 'Amazon order', 8, 120], + ['Cafe Central', 'Coffee', 3, 9], + ['Lidl', 'Lidl Einkauf', 8, 45], + ['DeepSeek', 'DeepSeek API credits', 5, 25], +] + +function dayList(from: string, to: string): string[] { + const start = new Date(from + 'T00:00:00Z').getTime() + const days = Math.min( + Math.floor((new Date(to + 'T00:00:00Z').getTime() - start) / MS_PER_DAY) + 1, + TEST_BANK_MAX_MOCK_HISTORY_DAYS, + ) + + return Array.from({ length: Math.max(days, 0) }, (_, i) => + new Date(start + i * MS_PER_DAY).toISOString().slice(0, 10), + ) +} + +@Injectable() +export class TestBankProvider implements BankProvider { + readonly name = 'mock' as const + private pending = new Map() // state -> request + private codes = new Map() // code -> bank + + async listBanks(country: string) { + return TEST_BANKS.filter(b => b.country === country.toUpperCase()) + } + + async startAuth(p: { bank: BankInfo; redirectUrl: string; state: string; validUntil: Date }) { + this.pending.set(p.state, { redirectUrl: p.redirectUrl, bank: p.bank }) + + return { + url: `/api/banking/mock-bank?state=${encodeURIComponent(p.state)}`, + } + } + + // ---- used by the mock bank pages ---- + pendingFor(state: string) { + return this.pending.get(state) + } + + decide(state: string, approve: boolean): string | null { + const req = this.pending.get(state) + + if (!req) { + return null + } + this.pending.delete(state) + const sep = req.redirectUrl.includes('?') ? '&' : '?' + + if (!approve || req.bank.name.includes('always denies')) { + return `${req.redirectUrl}${sep}error=access_denied&state=${encodeURIComponent(state)}` + } + const code = randomBytes(12).toString('hex') + + this.codes.set(code, req.bank) + + return `${req.redirectUrl}${sep}code=${code}&state=${encodeURIComponent(state)}` + } + + async completeAuth(code: string): Promise { + const bank = this.codes.get(code) + + if (!bank) { + throw new ProviderError('Unknown code', 'rejected', 400) + } + this.codes.delete(code) + const slug = bank.name.replace(/\W+/g, '').toLowerCase() + const accounts: ProviderAccount[] = + bank.name === 'Wise' + ? [ + { + uid: `${slug}-usd-${randomBytes(3).toString('hex')}`, + stableId: `${slug}-usd`, + label: '…0377', + currency: 'USD', + iban: 'DE02120300000000000377', + }, + { + uid: `${slug}-eur-${randomBytes(3).toString('hex')}`, + stableId: `${slug}-eur`, + label: '…4410', + currency: 'EUR', + iban: 'DE02120300000000004410', + }, + ] + : [ + { + uid: `${slug}-main-${randomBytes(3).toString('hex')}`, + stableId: `${slug}-main`, + label: '…4821', + currency: 'EUR', + iban: 'DE02120300000000004821', + }, + ] + + return { + sessionId: `mock-${randomBytes(8).toString('hex')}`, + accounts, + validUntil: isoDaysFromNow(TEST_BANK_DEFAULT_CONSENT_DAYS), + } + } + + async fetchTransactions(p: { + session: ProviderSession + account: ProviderAccount + from: string + to: string + }): Promise { + if (p.session.sessionId.includes('fail')) { + throw new ProviderError('Bank unavailable', 'unavailable', 503) + } + const out: BankTransaction[] = [] + + for (const day of dayList(p.from, p.to)) { + const rnd = mulberry32(hash(p.account.stableId + day)) + const dom = Number(day.slice(8, 10)) + const add = (desc: string, who: string, amount: number, i: number) => + out.push({ + date: day, + description: desc, + amount: Math.round(amount * 100) / 100, + currency: p.account.currency || 'EUR', + recipient: who, + externalId: `${p.account.stableId}:${day}:${i}`, + accountId: p.account.stableId, + rawData: { source: 'mock' }, + }) + + if (dom === 1) { + add('Salary', 'Acme GmbH', 2800, 0) + } + if (dom === 5) { + add('Netflix subscription', 'Netflix', -9.99, 1) + } + if (rnd() < 0.55) { + const [who, desc, lo, hi] = SPEND[Math.floor(rnd() * SPEND.length)] + + add(desc, who, -(lo + rnd() * (hi - lo)), 2) + } + } + + return out + } + + async revoke(): Promise {} +} diff --git a/api/src/bank-connections/sync/bank-auto-sync.test.ts b/api/src/bank-connections/sync/bank-auto-sync.test.ts new file mode 100644 index 0000000..d5d466b --- /dev/null +++ b/api/src/bank-connections/sync/bank-auto-sync.test.ts @@ -0,0 +1,46 @@ +import { afterEach, describe, expect, it, vi } from 'vitest' +import type { BankingConfig } from '../config/bank-connections.tokens' +import { BankAutoSync } from './bank-auto-sync' +import type { BankSyncService } from './bank-sync.service' + +const make = (hours: number) => { + const syncDue = vi.fn<(hours: number) => Promise>(async () => 0) + const auto = new BankAutoSync({ syncDue } as unknown as BankSyncService, { autoSyncHours: hours } as BankingConfig) + + return { auto, syncDue } +} + +afterEach(() => vi.useRealTimers()) + +describe('scheduled sync timing', () => { + it('catches up about a minute after start, then on every check', async () => { + vi.useFakeTimers() + const { auto, syncDue } = make(12) + + auto.onModuleInit() + await vi.advanceTimersByTimeAsync(59_000) + expect(syncDue).not.toHaveBeenCalled() + await vi.advanceTimersByTimeAsync(2_000) + expect(syncDue).toHaveBeenCalledTimes(1) + expect(syncDue).toHaveBeenCalledWith(12) + await vi.advanceTimersByTimeAsync(30 * 60_000) + expect(syncDue).toHaveBeenCalledTimes(2) + auto.onModuleDestroy() + }) + + it('does nothing when switched off, and stops when the app does', async () => { + vi.useFakeTimers() + const off = make(0) + + off.auto.onModuleInit() + await vi.advanceTimersByTimeAsync(2 * 60 * 60_000) + expect(off.syncDue).not.toHaveBeenCalled() + + const on = make(12) + + on.auto.onModuleInit() + on.auto.onModuleDestroy() + await vi.advanceTimersByTimeAsync(2 * 60 * 60_000) + expect(on.syncDue).not.toHaveBeenCalled() + }) +}) diff --git a/api/src/bank-connections/sync/bank-auto-sync.ts b/api/src/bank-connections/sync/bank-auto-sync.ts new file mode 100644 index 0000000..06730d4 --- /dev/null +++ b/api/src/bank-connections/sync/bank-auto-sync.ts @@ -0,0 +1,51 @@ +import { Inject, Injectable, Logger, OnModuleDestroy, OnModuleInit } from '@nestjs/common' +import { AUTO_SYNC_CHECK_INTERVAL_MS, AUTO_SYNC_STARTUP_DELAY_MS } from '../constants' +import { BankSyncService } from './bank-sync.service' +import { BANKING_CONFIG, BankingConfig } from '../config/bank-connections.tokens' + +/** Quietly catches up every active connection, so history builds without anyone pressing Sync. */ +@Injectable() +export class BankAutoSync implements OnModuleInit, OnModuleDestroy { + private readonly logger = new Logger('BankAutoSync') + private timer?: NodeJS.Timeout + private startup?: NodeJS.Timeout + private running = false + + constructor( + private readonly syncs: BankSyncService, + @Inject(BANKING_CONFIG) private readonly config: BankingConfig, + ) {} + + onModuleInit() { + if (!this.config.autoSyncHours) { + return + } + this.timer = setInterval(() => void this.tick(), AUTO_SYNC_CHECK_INTERVAL_MS) + this.timer.unref() // never keeps the process alive + // If the app was stopped for a while, don't wait a whole check interval to catch up + this.startup = setTimeout(() => void this.tick(), AUTO_SYNC_STARTUP_DELAY_MS) + this.startup.unref() + this.logger.log(`Scheduled sync every ${this.config.autoSyncHours}h`) + } + + onModuleDestroy() { + clearInterval(this.timer) + clearTimeout(this.startup) + } + + async tick() { + if (this.running) { + return 0 + } + this.running = true + try { + return await this.syncs.syncDue(this.config.autoSyncHours) + } catch (e) { + this.logger.error(e instanceof Error ? e.message : String(e)) + + return 0 + } finally { + this.running = false + } + } +} diff --git a/api/src/bank-connections/sync/bank-sync.controller.ts b/api/src/bank-connections/sync/bank-sync.controller.ts new file mode 100644 index 0000000..0dadb41 --- /dev/null +++ b/api/src/bank-connections/sync/bank-sync.controller.ts @@ -0,0 +1,33 @@ +import { Body, Controller, HttpCode, Inject, Param, Post, Req, UseGuards, UseInterceptors } from '@nestjs/common' +import type { Request } from 'express' +import { AccessGuard, CurrentActor } from '../../access/access.guard' +import { ACCESS_SETTINGS, AccessSettings, Actor } from '../../access/access.settings' +import { CsrfGuard } from '../../access/csrf.guard' +import { NoStoreInterceptor } from '../../shared/http/no-store.interceptor' +import { ZodValidationPipe } from '../../shared/http/zod-validation.pipe' +import { SyncInput, SyncSchema } from '../connections/bank-connections.schemas' +import { psuFromRequest } from './psu' +import { BankSyncService } from './bank-sync.service' + +/** Pressing Sync: fetch now, keep what is new, and return what the bank sent. */ +@Controller('api/banking') +@UseGuards(AccessGuard, CsrfGuard) +@UseInterceptors(NoStoreInterceptor) +export class BankSyncController { + constructor( + private readonly syncs: BankSyncService, + @Inject(ACCESS_SETTINGS) private readonly access: AccessSettings, + ) {} + + @Post('connections/:id/sync') + @HttpCode(200) // the previous API answered 200, not 201 + sync( + @CurrentActor() actor: Actor, + @Param('id') id: string, + @Body(new ZodValidationPipe(SyncSchema, { message: 'Invalid dates' })) + body: SyncInput, + @Req() req: Request, + ) { + return this.syncs.sync(actor, id, body ?? {}, psuFromRequest(req, this.access.mode === 'cloudflare')) + } +} diff --git a/api/src/bank-connections/sync/bank-sync.service.ts b/api/src/bank-connections/sync/bank-sync.service.ts new file mode 100644 index 0000000..10a39ca --- /dev/null +++ b/api/src/bank-connections/sync/bank-sync.service.ts @@ -0,0 +1,176 @@ +import { + BadRequestException, + ConflictException, + HttpException, + Inject, + Injectable, + NotFoundException, +} from '@nestjs/common' +import { Actor } from '../../access/access.settings' +import { accountRef, decrypt } from '../../shared/crypto/encryption' +import { addDays, addMilliseconds, MS_PER_DAY } from '../../shared/constants/time' +import { BANKING_CONFIG, BankingConfig, CLOCK, Clock } from '../config/bank-connections.tokens' +import { connectionStatus } from '../connections/connection-status' +import { SyncInput } from '../connections/bank-connections.schemas' +import { BankConnectionsStore, ConnectionRow } from '../connections/bank-connections.store' +import { + AUTO_SYNC_LOOKBACK_MS, + DEFAULT_SYNC_HISTORY_DAYS, + MAX_SYNC_RANGE_DAYS, + MIN_SYNC_INTERVAL_MS, + SYNC_OVERLAP_DAYS, +} from '../constants' +import { + BANK_PROVIDER, + BankProvider, + BankTransaction, + ProviderError, + ProviderSession, + PsuInfo, +} from '../providers/bank-provider' +import { BankTransactionsStore } from '../transactions/bank-transactions.store' + +const day = (d: Date) => d.toISOString().slice(0, 10) + +/** Fetches transactions from the bank and keeps them: on request, and as a scheduled catch-up. */ +@Injectable() +export class BankSyncService { + constructor( + private readonly store: BankConnectionsStore, + private readonly txs: BankTransactionsStore, + @Inject(BANK_PROVIDER) private readonly provider: BankProvider, + @Inject(BANKING_CONFIG) private readonly config: BankingConfig, + @Inject(CLOCK) private readonly clock: Clock, + ) {} + + private now() { + return this.clock.now() + } + + async sync(actor: Actor, id: string, input: SyncInput, psu?: PsuInfo) { + const row = this.store.getOwnedConnection(id, actor.userId) + + if (!row || row.status === 'revoked' || row.status === 'replaced') { + throw new NotFoundException({ error: 'Not found' }) + } + if (connectionStatus(row, this.now()).status === 'expired') { + throw new ConflictException({ error: 'expired' }) + } + + const today = day(this.now()) + const to = input.to && input.to < today ? input.to : today + const from = + input.from ?? + (row.last_sync_to + ? day(addDays(new Date(row.last_sync_to), -SYNC_OVERLAP_DAYS)) + : day(addDays(this.now(), -DEFAULT_SYNC_HISTORY_DAYS))) + + if (from > to || (new Date(to).getTime() - new Date(from).getTime()) / MS_PER_DAY > MAX_SYNC_RANGE_DAYS) { + throw new BadRequestException({ error: 'Invalid date range' }) + } + + // Banks limit how often access without a user present is allowed; stay well inside that + if ( + row.last_sync_started_at && + this.now().getTime() - new Date(row.last_sync_started_at).getTime() < MIN_SYNC_INTERVAL_MS + ) { + throw new HttpException({ error: 'Please wait a minute before syncing again.' }, 429) + } + + // A connection's very first fetch asks for all the history the bank will give + return this.runSync(row, from, to, actor.email, { psu, longest: !row.last_sync_to && !input.from }) + } + + /** Fetches, stores and logs one sync. Used by people pressing Sync and by the scheduled catch-up. */ + private async runSync( + row: ConnectionRow, + from: string, + to: string, + who: string, + opts: { psu?: PsuInfo; longest?: boolean } = {}, + ) { + const userId = row.user_id! + + this.store.markSyncStarted(row.id, this.now().toISOString()) + const logId = this.store.startSyncLog(row.id, from, to) + + try { + const secret = JSON.parse(decrypt(row.secret_blob, this.config.encryptionKey)) as Pick< + ProviderSession, + 'sessionId' | 'accounts' | 'requiredPsuHeaders' + > + const session: ProviderSession = { + ...secret, + validUntil: row.valid_until, + } + const all: BankTransaction[] = [] + + for (const account of session.accounts) { + const fetched = await this.provider.fetchTransactions({ + session, + account, + from, + to, + psu: opts.psu, + longest: opts.longest, + }) + + // Stamp where each one came from, so the app can browse bank → account → transactions + for (const tx of fetched) { + tx.bankName = row.aspsp_name + tx.accountLabel = account.label + tx.accountRef = accountRef(account.stableId) + } + all.push(...fetched) + } + all.sort((a, b) => b.date.localeCompare(a.date)) + const added = this.txs.save(userId, row.id, all) + + this.store.markSyncDone(row.id, to, this.now().toISOString()) + this.store.finishSyncLog(logId, 'ok', all.length) + this.store.audit(who, 'sync', row.aspsp_name, { fetched: all.length, added }, userId) + + return { transactions: all, from, to, count: all.length, added } + } catch (e) { + const kind = e instanceof ProviderError ? e.kind : 'unavailable' + + if (kind === 'consent_expired') { + this.store.setStatus(row.id, 'expired') + } + this.store.finishSyncLog(logId, kind === 'rate_limited' ? 'rate_limited' : 'failed', 0, kind) + this.store.audit(who, 'sync.failed', row.aspsp_name, { kind }, userId) + const status = kind === 'consent_expired' ? 409 : kind === 'rate_limited' ? 429 : kind === 'too_large' ? 422 : 502 + + throw new HttpException({ error: kind === 'consent_expired' ? 'expired' : kind }, status) + } + } + + /** + * The catch-up: every active connection that hasn't synced for `everyHours`, one at a time. + * One failing connection never stops the others, and an expired consent just flips the status. + */ + async syncDue(everyHours: number) { + const now = this.now() + const due = this.store.dueForSync( + now.toISOString(), + addMilliseconds(now, -AUTO_SYNC_LOOKBACK_MS(everyHours)).toISOString(), + ) + const today = day(now) + let done = 0 + + for (const row of due) { + const from = row.last_sync_to + ? day(addDays(new Date(row.last_sync_to), -SYNC_OVERLAP_DAYS)) + : day(addDays(now, -DEFAULT_SYNC_HISTORY_DAYS)) + + try { + await this.runSync(row, from, today, 'scheduler', { longest: !row.last_sync_to }) + done++ + } catch { + /* already logged and audited; try the next one */ + } + } + + return done + } +} diff --git a/api/src/bank-connections/sync/psu.test.ts b/api/src/bank-connections/sync/psu.test.ts new file mode 100644 index 0000000..aebd586 --- /dev/null +++ b/api/src/bank-connections/sync/psu.test.ts @@ -0,0 +1,21 @@ +import { describe, expect, it } from 'vitest' +import type { Request } from 'express' +import { psuFromRequest } from './psu' + +const req = (headers: Record, ip?: string) => + ({ header: (n: string) => headers[n.toLowerCase()], ip }) as unknown as Request + +describe('psuFromRequest', () => { + it('uses the Cloudflare address behind Cloudflare, the connection address otherwise', () => { + const r = req({ 'cf-connecting-ip': '203.0.113.7', 'user-agent': 'UA' }, '10.0.0.2') + + expect(psuFromRequest(r, true)).toEqual({ ip: '203.0.113.7', userAgent: 'UA' }) + expect(psuFromRequest(r, false).ip).toBe('10.0.0.2') + }) + it('drops addresses that are not addresses, and cleans the browser string', () => { + expect(psuFromRequest(req({ 'cf-connecting-ip': 'not-an-ip; DROP' }), true).ip).toBeUndefined() + expect(psuFromRequest(req({ 'user-agent': 'Moz\r\nX: y' }), true).userAgent).toBe('MozX: y') + expect(psuFromRequest(req({}, '::ffff:198.51.100.4'), false).ip).toBe('198.51.100.4') + expect(psuFromRequest(req({ 'user-agent': 'a'.repeat(900) }), true).userAgent).toHaveLength(512) + }) +}) diff --git a/api/src/bank-connections/sync/psu.ts b/api/src/bank-connections/sync/psu.ts new file mode 100644 index 0000000..3a3ab8e --- /dev/null +++ b/api/src/bank-connections/sync/psu.ts @@ -0,0 +1,18 @@ +import { isIP } from 'node:net' +import type { Request } from 'express' +import type { PsuInfo } from '../providers/bank-provider' + +/** + * The person's own IP address and browser, for the bank. Behind Cloudflare Access the real address + * is in Cf-Connecting-IP; anywhere else we use the connection's own. Anything odd is left out. + */ +export function psuFromRequest(req: Request, behindCloudflare: boolean): PsuInfo { + const raw = (behindCloudflare ? req.header('cf-connecting-ip') : req.ip)?.trim().replace(/^::ffff:/, '') + const ua = Array.from(req.header('user-agent') ?? '') + .filter(ch => ch.charCodeAt(0) > 31 && ch.charCodeAt(0) !== 127) + .join('') + .trim() + .slice(0, 512) + + return { ip: raw && isIP(raw) ? raw : undefined, userAgent: ua || undefined } +} diff --git a/api/src/bank-connections/transactions/bank-transactions.store.ts b/api/src/bank-connections/transactions/bank-transactions.store.ts new file mode 100644 index 0000000..feafcf7 --- /dev/null +++ b/api/src/bank-connections/transactions/bank-transactions.store.ts @@ -0,0 +1,152 @@ +import { Inject, Injectable } from '@nestjs/common' +import { createHmac } from 'node:crypto' +import { DatabaseService } from '../../shared/database/database.service' +import { decrypt, encrypt } from '../../shared/crypto/encryption' +import { hasColumn } from '../../shared/database/migrate' +import { BankTransaction } from '../providers/bank-provider' +import { BANKING_CONFIG, BankingConfig } from '../config/bank-connections.tokens' + +/** + * Synced transactions, kept per person. Every query filters by user_id; nothing here takes an + * identity from a request. Everything about a transaction (date, amount, text, names, IBANs, + * account details) is encrypted in one value. What stays readable is only who owns the row, which + * connection it came from, when it was stored, and an opaque key used to skip repeats. + */ +@Injectable() +export class BankTransactionsStore { + private readonly q + private readonly key: Buffer + private readonly dedupeKey: Buffer + + constructor(database: DatabaseService, @Inject(BANKING_CONFIG) config: BankingConfig) { + const { db } = database + + this.key = config.encryptionKey + // A separate key for the repeat-skipping fingerprint, so it never reveals what a row contains + this.dedupeKey = createHmac('sha256', this.key).update('finkit:tx-dedupe').digest() + const upgrade = hasColumn(db, 'bank_transactions', 'date') + + if (upgrade) { + // Older layout kept date, amount and currency readable: set it aside and move the rows over + db.exec(` + ALTER TABLE bank_transactions RENAME TO bank_transactions_v1; + DROP INDEX IF EXISTS idx_bank_tx_unique; + DROP INDEX IF EXISTS idx_bank_tx_user_date; + `) + } + db.exec(` + CREATE TABLE IF NOT EXISTS bank_transactions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id TEXT NOT NULL, + connection_id TEXT NOT NULL, + ext_key TEXT NOT NULL, + payload TEXT NOT NULL, + created_at TEXT NOT NULL + ); + CREATE UNIQUE INDEX IF NOT EXISTS idx_bank_tx_unique ON bank_transactions(user_id, ext_key); + `) + this.q = { + insert: db.prepare( + `INSERT OR IGNORE INTO bank_transactions (user_id, connection_id, ext_key, payload, created_at) + VALUES (?,?,?,?,?)`, + ), + list: db.prepare('SELECT payload FROM bank_transactions WHERE user_id = ? ORDER BY id DESC'), + count: db.prepare('SELECT COUNT(*) AS n FROM bank_transactions WHERE user_id = ?'), + move: db.prepare('UPDATE bank_transactions SET connection_id = ? WHERE user_id = ? AND connection_id = ?'), + deleteConn: db.prepare('DELETE FROM bank_transactions WHERE user_id = ? AND connection_id = ?'), + deleteAll: db.prepare('DELETE FROM bank_transactions WHERE user_id = ?'), + } + this.insertMany = db.transaction((userId: string, connectionId: string, txs: BankTransaction[]) => { + let added = 0 + + for (const tx of txs) { + added += this.q.insert.run( + userId, + connectionId, + this.fingerprint(tx), + encrypt(JSON.stringify(tx), this.key), + new Date().toISOString(), + ).changes + } + + return added + }) + if (upgrade) { + db.transaction(() => { + const old = db.prepare('SELECT * FROM bank_transactions_v1 ORDER BY id').all() as Array<{ + user_id: string + connection_id: string + date: string + amount: number + currency: string + payload: string + created_at: string + }> + + for (const r of old) { + const tx = { + ...JSON.parse(decrypt(r.payload, this.key)), + date: r.date, + amount: r.amount, + currency: r.currency, + } as BankTransaction + + this.q.insert.run( + r.user_id, + r.connection_id, + this.fingerprint(tx), + encrypt(JSON.stringify(tx), this.key), + r.created_at, + ) + } + db.exec('DROP TABLE bank_transactions_v1') + })() + } + } + + private readonly insertMany: (userId: string, connectionId: string, txs: BankTransaction[]) => number + + /** Stores what is new and ignores what is already there. Returns how many were new. */ + save(userId: string, connectionId: string, txs: BankTransaction[]): number { + return this.insertMany(userId, connectionId, txs) + } + + /** Newest first. The date range is applied after decrypting, because dates are not readable in the database. */ + list(userId: string, from = '0000-01-01', to = '9999-12-31'): BankTransaction[] { + const rows = this.q.list.all(userId) as Array<{ payload: string }> + + return rows + .map(r => JSON.parse(decrypt(r.payload, this.key)) as BankTransaction) + .filter(t => t.date >= from && t.date <= to) + .toSorted((a, b) => b.date.localeCompare(a.date)) // stable: same-day rows keep newest-stored first + } + + count(userId: string): number { + return (this.q.count.get(userId) as { n: number }).n + } + + /** A reconnect makes a new connection for the same accounts: keep the history under the new one. */ + moveConnection(userId: string, from: string, to: string) { + this.q.move.run(to, userId, from) + } + + deleteForConnection(userId: string, connectionId: string): number { + return this.q.deleteConn.run(userId, connectionId).changes + } + + deleteAll(userId: string): number { + return this.q.deleteAll.run(userId).changes + } + + /** + * An opaque id used to skip repeats: the bank's own reference when there is one, otherwise the + * row's content. Keyed, so neither an IBAN inside a reference nor a guessable payment can be read from it. + */ + private fingerprint(tx: BankTransaction): string { + const material = tx.externalId + ? `e:${tx.externalId}` + : `h:${[tx.accountRef ?? '', tx.date, tx.amount, tx.description, tx.recipient ?? ''].join('|')}` + + return createHmac('sha256', this.dedupeKey).update(material).digest('hex') + } +} diff --git a/api/src/bank-connections/transactions/stored-transactions.controller.ts b/api/src/bank-connections/transactions/stored-transactions.controller.ts new file mode 100644 index 0000000..a97ea33 --- /dev/null +++ b/api/src/bank-connections/transactions/stored-transactions.controller.ts @@ -0,0 +1,31 @@ +import { BadRequestException, Controller, Delete, Get, Query, UseGuards, UseInterceptors } from '@nestjs/common' +import { AccessGuard, CurrentActor } from '../../access/access.guard' +import { Actor } from '../../access/access.settings' +import { CsrfGuard } from '../../access/csrf.guard' +import { NoStoreInterceptor } from '../../shared/http/no-store.interceptor' +import { StoredTransactionsService } from './stored-transactions.service' + +/** What the server holds for the signed-in user. */ +@Controller('api/banking') +@UseGuards(AccessGuard, CsrfGuard) +@UseInterceptors(NoStoreInterceptor) +export class StoredTransactionsController { + constructor(private readonly stored: StoredTransactionsService) {} + + /** Everything stored for the signed-in user (optionally one date range). */ + @Get('transactions') + list(@CurrentActor() actor: Actor, @Query('from') from?: string, @Query('to') to?: string) { + const date = /^\d{4}-\d{2}-\d{2}$/ + + if ((from && !date.test(from)) || (to && !date.test(to))) { + throw new BadRequestException({ error: 'Invalid dates' }) + } + + return this.stored.list(actor, from, to) + } + + @Delete('transactions') + deleteAll(@CurrentActor() actor: Actor) { + return this.stored.deleteAll(actor) + } +} diff --git a/api/src/bank-connections/transactions/stored-transactions.service.ts b/api/src/bank-connections/transactions/stored-transactions.service.ts new file mode 100644 index 0000000..acbaa29 --- /dev/null +++ b/api/src/bank-connections/transactions/stored-transactions.service.ts @@ -0,0 +1,29 @@ +import { Injectable } from '@nestjs/common' +import { Actor } from '../../access/access.settings' +import { BankConnectionsStore } from '../connections/bank-connections.store' +import { BankTransactionsStore } from './bank-transactions.store' + +/** What the server has kept for a person: reading it and erasing it. Always limited to that person. */ +@Injectable() +export class StoredTransactionsService { + constructor( + private readonly store: BankConnectionsStore, + private readonly txs: BankTransactionsStore, + ) {} + + /** What has been stored for this person, optionally within a date range. */ + list(actor: Actor, from?: string, to?: string) { + const transactions = this.txs.list(actor.userId, from, to) + + return { transactions, count: transactions.length } + } + + /** Removes this person's stored transactions (their connections stay). */ + deleteAll(actor: Actor) { + const deleted = this.txs.deleteAll(actor.userId) + + this.store.audit(actor.email, 'transactions.delete', undefined, { deleted }, actor.userId) + + return { deleted } + } +} diff --git a/api/src/bank-connections/transactions/stored-transactions.test.ts b/api/src/bank-connections/transactions/stored-transactions.test.ts new file mode 100644 index 0000000..046cc32 --- /dev/null +++ b/api/src/bank-connections/transactions/stored-transactions.test.ts @@ -0,0 +1,289 @@ +import { afterEach, describe, expect, it } from 'vitest' +import request from 'supertest' +import { mkdtempSync, rmSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { encrypt } from '../../shared/crypto/encryption' +import { SIGNED_IN_HEADERS as H, TEST_KEY, TestAppOptions, createTestApp } from '../../testing/test-app' +import { BankAutoSync } from '../sync/bank-auto-sync' +import { BankSyncService } from '../sync/bank-sync.service' +import { TestBankProvider } from '../providers/test-bank.provider' + +type TestApp = Awaited> +const open: TestApp[] = [] + +async function make(options?: TestAppOptions) { + const t = await createTestApp({ ...options, env: { ALLOWED_USER_EMAILS: `${ALICE},${BOB}`, ...options?.env } }) + + open.push(t) + + return t +} +afterEach(async () => { + while (open.length) { + await open.pop()!.close() + } +}) + +const ALICE = 'alice@x.com' +const BOB = 'bob@x.com' +const as = (who: string) => ({ ...H, 'X-Dev-User': who }) +const loc = (res: request.Response) => String(res.headers.location ?? '') +const cookieOf = (res: request.Response) => String(res.headers['set-cookie']?.[0] ?? '').split(';')[0] + +async function connect(t: TestApp, who: string, extra: Record = {}) { + const start = await request(t.server) + .post('/api/banking/connect') + .set(as(who)) + .send({ bank: 'N26', ...extra }) + .expect(200) + const state = new URL(start.body.url, 'http://x').searchParams.get('state')! + const decide = await request(t.server) + .get(`/api/banking/mock-bank/decide?state=${encodeURIComponent(state)}&d=approve`) + .expect(302) + const back = await request(t.server).get(loc(decide)).set('Cookie', cookieOf(start)) + + return new URL(loc(back), 'http://x').searchParams.get('c')! +} +const sync = (t: TestApp, who: string, id: string) => + request(t.server).post(`/api/banking/connections/${id}/sync`).set(as(who)).send({}) +const stored = async (t: TestApp, who: string) => + (await request(t.server).get('/api/banking/transactions').set(as(who)).expect(200)).body + +describe('stored transactions', () => { + it('keeps what a sync fetched, and a repeat sync adds nothing', async () => { + let now = new Date('2026-10-20T12:00:00Z') + const t = await make({ clock: { now: () => now } }) + const id = await connect(t, ALICE) + const first = (await sync(t, ALICE, id).expect(200)).body + + expect((await stored(t, ALICE)).count).toBe(first.count) + expect(first.added).toBe(first.count) + now = new Date('2026-10-20T12:05:00Z') + const again = (await sync(t, ALICE, id).expect(200)).body + + expect(again.added).toBe(0) + expect((await stored(t, ALICE)).count).toBe(first.count) + }) + + it('filters by date range and rejects malformed dates', async () => { + const t = await make() + + await sync(t, ALICE, await connect(t, ALICE)) + const all = await stored(t, ALICE) + const one = all.transactions[0].date + const day = (await request(t.server).get(`/api/banking/transactions?from=${one}&to=${one}`).set(as(ALICE))).body + + expect(day.count).toBeGreaterThan(0) + expect(day.count).toBeLessThanOrEqual(all.count) + expect(day.transactions.every((x: { date: string }) => x.date === one)).toBe(true) + await request(t.server).get('/api/banking/transactions?from=yesterday').set(as(ALICE)).expect(400) + }) + + it('keeps every part of a transaction encrypted at rest, with an opaque repeat key', async () => { + const t = await make() + + await sync(t, ALICE, await connect(t, ALICE)) + const rows = t.db.prepare('SELECT * FROM bank_transactions').all() as Array> + // created_at is when we stored the row (it can coincide with a booking date), so it is left out of the content check + const dump = JSON.stringify(rows.map(r => ({ ...r, created_at: undefined }))) + const first = (await stored(t, ALICE)).transactions[0] + + expect(Object.keys(rows[0]).toSorted()).toEqual([ + 'connection_id', + 'created_at', + 'ext_key', + 'id', + 'payload', + 'user_id', + ]) + expect(dump).not.toMatch(/REWE|Netflix|Salary|Acme|Amazon|Shell|DE\d{20}/) + expect(dump).not.toContain(first.date) // dates are not readable either + expect(String(rows[0].payload)).toMatch(/^v1\./) + expect(String(rows[0].ext_key)).toMatch(/^[0-9a-f]{64}$/) // not the bank's reference, nor derived readably from it + expect(dump).not.toContain(String(first.externalId)) + }) + + it('shows each person only their own, and one person cannot delete another’s', async () => { + const t = await make() + + await sync(t, ALICE, await connect(t, ALICE)) + await sync(t, BOB, await connect(t, BOB)) + const alice = await stored(t, ALICE) + const bob = await stored(t, BOB) + + expect(alice.count).toBeGreaterThan(0) + expect(bob.count).toBeGreaterThan(0) + // Same test bank, so the same rows are stored once each, never shared between them + expect(t.db.prepare('SELECT COUNT(*) AS n FROM bank_transactions').get()).toEqual({ n: alice.count + bob.count }) + expect((await request(t.server).delete('/api/banking/transactions').set(as(BOB)).expect(200)).body.deleted).toBe( + bob.count, + ) + expect((await stored(t, BOB)).count).toBe(0) + expect((await stored(t, ALICE)).count).toBe(alice.count) + }) + + it('is removed with its connection, and survives a reconnect', async () => { + const now = new Date('2026-10-20T12:00:00Z') + const t = await make({ clock: { now: () => now } }) + const first = await connect(t, ALICE) + + await sync(t, ALICE, first) + const before = (await stored(t, ALICE)).count + const second = await connect(t, ALICE, { reconnect: first }) + + expect((await stored(t, ALICE)).count).toBe(before) // history carried over + await request(t.server).delete(`/api/banking/connections/${second}`).set(as(ALICE)).expect(200) + expect((await stored(t, ALICE)).count).toBe(0) + }) +}) + +describe('upgrading older stored data', () => { + it('moves rows from the layout that kept date and amount readable, without losing or duplicating any', async () => { + const dir = mkdtempSync(join(tmpdir(), 'finkit-')) + const file = join(dir, 'db.sqlite') + + try { + const first = await createTestApp({ databaseFile: file, env: { ALLOWED_USER_EMAILS: ALICE } }) + + await request(first.server).get('/api/banking/connections').set(as(ALICE)).expect(200) + const userId = (first.db.prepare('SELECT id FROM users WHERE email = ?').get(ALICE) as { id: string }).id + + // Put it back the way the first version stored it + first.db.exec('DROP TABLE bank_transactions') + first.db.exec(`CREATE TABLE bank_transactions (id INTEGER PRIMARY KEY AUTOINCREMENT, user_id TEXT NOT NULL, + connection_id TEXT NOT NULL, ext_key TEXT NOT NULL, date TEXT NOT NULL, amount REAL NOT NULL, + currency TEXT NOT NULL, payload TEXT NOT NULL, created_at TEXT NOT NULL); + CREATE UNIQUE INDEX idx_bank_tx_unique ON bank_transactions(user_id, ext_key); + CREATE INDEX idx_bank_tx_user_date ON bank_transactions(user_id, date)`) + const put = first.db.prepare('INSERT INTO bank_transactions VALUES (NULL,?,?,?,?,?,?,?,?)') + const body = (d: string) => + encrypt(JSON.stringify({ description: d, externalId: `acc:${d}`, rawData: {} }), TEST_KEY) + + put.run(userId, 'c1', 'acc:old1', '2026-09-01', -12.5, 'EUR', body('old1'), '2026-09-02T00:00:00Z') + put.run(userId, 'c1', 'acc:old2', '2026-09-03', 99, 'EUR', body('old2'), '2026-09-04T00:00:00Z') + await first.close() + + const second = await createTestApp({ databaseFile: file, env: { ALLOWED_USER_EMAILS: ALICE } }) + + open.push(second) + const list = (await stored(second, ALICE)).transactions + + expect( + list.map((x: { description: string; date: string; amount: number }) => [x.description, x.date, x.amount]), + ).toEqual([ + ['old2', '2026-09-03', 99], + ['old1', '2026-09-01', -12.5], + ]) + expect(Object.keys(second.db.prepare('SELECT * FROM bank_transactions LIMIT 1').get() as object)).not.toContain( + 'date', + ) + expect( + second.db.prepare("SELECT name FROM sqlite_master WHERE name = 'bank_transactions_v1'").get(), + ).toBeUndefined() + } finally { + rmSync(dir, { recursive: true, force: true }) + } + }) +}) + +describe('connections made before the account IBAN was kept', () => { + it('suggest a reconnect, new ones do not', async () => { + const t = await make() + const id = await connect(t, ALICE) + const list = async () => + (await request(t.server).get('/api/banking/connections').set(as(ALICE))).body.connections[0] + + expect((await list()).reconnectSuggested).toBe(false) + expect(JSON.stringify(await list())).not.toMatch(/ownIban|DE\d{20}/) // nothing extra reaches the browser + t.db + .prepare('UPDATE bank_connections SET account_labels = ? WHERE id = ?') + .run(JSON.stringify([{ id: 'a1', label: '…1234', currency: 'EUR' }]), id) + expect((await list()).reconnectSuggested).toBe(true) + }) +}) + +describe('telling the bank who is syncing', () => { + class Spy extends TestBankProvider { + seen: Array<{ psu?: unknown; longest?: boolean; required?: string[] }> = [] + + async listBanks(country: string) { + return (await super.listBanks(country)).map(b => ({ ...b, requiredPsuHeaders: ['Psu-Ip-Address'] })) + } + + async fetchTransactions(p: Parameters[0]) { + this.seen.push({ + psu: (p as { psu?: unknown }).psu, + longest: (p as { longest?: boolean }).longest, + required: p.session.requiredPsuHeaders, + }) + + return super.fetchTransactions(p) + } + } + + it('passes the person and the bank’s needs to a manual sync, and nothing to a scheduled one', async () => { + const provider = new Spy() + let now = new Date('2026-10-20T12:00:00Z') + const t = await make({ provider, clock: { now: () => now } }) + const id = await connect(t, ALICE) + + await request(t.server) + .post(`/api/banking/connections/${id}/sync`) + .set(as(ALICE)) + .set('User-Agent', 'TestBrowser/1.0') + .send({}) + .expect(200) + expect(provider.seen[0]).toMatchObject({ + required: ['Psu-Ip-Address'], + longest: true, // the very first fetch + psu: { userAgent: 'TestBrowser/1.0' }, + }) + now = new Date('2026-10-21T04:00:00Z') + await t.app.get(BankSyncService).syncDue(12) + expect(provider.seen.at(-1)).toMatchObject({ psu: undefined, longest: false }) + }) +}) + +describe('scheduled catch-up', () => { + it('syncs due connections only, for the right person, and then leaves them alone', async () => { + let now = new Date('2026-10-20T12:00:00Z') + const t = await make({ clock: { now: () => now } }) + const id = await connect(t, ALICE) + + await connect(t, BOB) + const svc = t.app.get(BankSyncService) + + expect(await svc.syncDue(12)).toBe(2) // never synced: both are due + expect((await stored(t, ALICE)).count).toBeGreaterThan(0) + expect((await stored(t, BOB)).count).toBeGreaterThan(0) + now = new Date('2026-10-20T20:00:00Z') + expect(await svc.syncDue(12)).toBe(0) // only 8 hours later + now = new Date('2026-10-21T01:00:00Z') + expect(await svc.syncDue(12)).toBe(2) + expect( + t.store!.auditLog(t.db.prepare('SELECT id FROM users WHERE email = ?').pluck().get(ALICE) as string), + ).toEqual(expect.arrayContaining([expect.objectContaining({ actor: 'scheduler', action: 'sync' })])) + expect(id).toBeTruthy() + }) + + it('marks an expired consent and keeps going', async () => { + let now = new Date('2026-10-20T12:00:00Z') + const t = await make({ clock: { now: () => now } }) + const id = await connect(t, ALICE) + + await connect(t, BOB) + t.store!.setValidUntil(id, '2026-10-21T00:00:00Z') + now = new Date('2026-10-22T12:00:00Z') // Alice's consent has run out + const svc = t.app.get(BankSyncService) + + expect(await svc.syncDue(12)).toBe(1) // Bob's still synced + expect(t.app.get(BankAutoSync)).toBeDefined() + }) + + it('does not start without an interval', async () => { + const t = await make({ env: { BANKING_AUTO_SYNC_HOURS: '0' } }) + + expect(await t.app.get(BankAutoSync).tick()).toBe(0) + }) +}) diff --git a/api/src/health/health.controller.ts b/api/src/health/health.controller.ts new file mode 100644 index 0000000..2903d65 --- /dev/null +++ b/api/src/health/health.controller.ts @@ -0,0 +1,15 @@ +import { Controller, Get } from '@nestjs/common' + +const status = () => ({ status: 'ok', timestamp: new Date().toISOString() }) + +// Used by the container health check (/health) and by anything behind the /api proxy +@Controller() +export class HealthController { + @Get('health') plain() { + return status() + } + + @Get('api/health') api() { + return status() + } +} diff --git a/api/src/health/health.module.ts b/api/src/health/health.module.ts new file mode 100644 index 0000000..8794680 --- /dev/null +++ b/api/src/health/health.module.ts @@ -0,0 +1,5 @@ +import { Module } from '@nestjs/common' +import { HealthController } from './health.controller' + +@Module({ controllers: [HealthController] }) +export class HealthModule {} diff --git a/api/src/index.ts b/api/src/index.ts deleted file mode 100644 index 9fc7fcf..0000000 --- a/api/src/index.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { serve } from '@hono/node-server' -import { Hono } from 'hono' -import { cors } from 'hono/cors' -import { logger } from 'hono/logger' -import { categorizeRoute } from './routes/categorize' -import { exportRoute } from './routes/export' -import { learnRoute } from './routes/learn' -import { settingsRoute } from './routes/settings' - -const app = new Hono() - -// Middleware -app.use('*', logger()) -app.use('*', cors({ - // In production, set ALLOWED_ORIGIN (e.g. https://finkit.example.com) to lock this down. - // Unset keeps the old behaviour (any origin), which is fine behind Cloudflare Access. - origin: process.env.NODE_ENV === 'production' - ? (process.env.ALLOWED_ORIGIN || '*') - : ['http://localhost:5173', 'http://localhost:3000'], - allowMethods: ['GET', 'POST', 'DELETE', 'OPTIONS'], - allowHeaders: ['Content-Type'], -})) - -// Health check -app.get('/health', (c) => { - return c.json({ status: 'ok', timestamp: new Date().toISOString() }) -}) -app.get('/api/health', (c) => { - return c.json({ status: 'ok', timestamp: new Date().toISOString() }) -}) - -// Routes - authentication handled by Cloudflare Access at edge -app.route('/api/categorize', categorizeRoute) -app.route('/api/export', exportRoute) -app.route('/api/learn', learnRoute) -app.route('/api/settings', settingsRoute) - -// 404 handler -app.notFound((c) => { - return c.json({ error: 'Not Found' }, 404) -}) - -// Error handler -app.onError((err, c) => { - console.error('Server error:', err) - return c.json({ error: 'Internal Server Error' }, 500) -}) - -const port = Number(process.env.PORT) || 3003 -console.log(`Server starting on port ${port}...`) - -serve({ - fetch: app.fetch, - port, -}) - -console.log(`Server running at http://localhost:${port}`) diff --git a/api/src/lib/anonymizer.ts b/api/src/lib/anonymizer.ts deleted file mode 100644 index 6941345..0000000 --- a/api/src/lib/anonymizer.ts +++ /dev/null @@ -1,72 +0,0 @@ -// Server-side validation to ensure data is anonymized before processing - -interface TransactionData { - id: string; - description: string; - amount: number; - date: string; - category?: string; -} - -interface AnonymizationValidation { - isValid: boolean; - issues: string[]; -} - -// Patterns that indicate non-anonymized personal data -const PERSONAL_DATA_PATTERNS = { - // Full IBAN (not anonymized) - fullIban: /[A-Z]{2}\d{2}[A-Z0-9]{4}\d{7}[A-Z0-9]{0,16}/g, - - // Email addresses - email: /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g, - - // Phone numbers - phone: /(?:\+\d{1,3}[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}/g, - - // German postal codes with city - addressWithPostal: /\d{5}\s+[A-Za-zäöüÄÖÜß]+/g, -}; - -// Patterns that indicate properly anonymized data -const ANONYMIZED_PATTERNS = { - // Anonymized IBAN: DE**************5515 - anonymizedIban: /[A-Z]{2}\*{10,}\d{4}/, - - // Pseudonymized name: Person_XXX - pseudonymizedName: /Person_[A-Z0-9]{3,}/, - - // Anonymized account: Acc_XXXXXX - anonymizedAccount: /Acc_[A-Z0-9]{6}/, -}; - -export function validateAnonymized(transactions: TransactionData[]): AnonymizationValidation { - const issues: string[] = []; - - for (const tx of transactions) { - // Check description for personal data - for (const [patternName, pattern] of Object.entries(PERSONAL_DATA_PATTERNS)) { - // Reset regex lastIndex for global patterns - pattern.lastIndex = 0; - - if (pattern.test(tx.description)) { - issues.push(`Transaction ${tx.id}: contains ${patternName} in description`); - } - } - } - - return { - isValid: issues.length === 0, - issues, - }; -} - -export function containsPersonalData(text: string): boolean { - for (const pattern of Object.values(PERSONAL_DATA_PATTERNS)) { - pattern.lastIndex = 0; - if (pattern.test(text)) { - return true; - } - } - return false; -} diff --git a/api/src/load-env.ts b/api/src/load-env.ts new file mode 100644 index 0000000..cbec5a3 --- /dev/null +++ b/api/src/load-env.ts @@ -0,0 +1,10 @@ +import { existsSync } from 'node:fs' + +/** Local runs read api/.env. Variables already set (Docker, shell, CI) always win. */ +export function loadLocalEnv(): void { + if (existsSync('.env') && typeof process.loadEnvFile === 'function') { + process.loadEnvFile('.env') + } +} + +loadLocalEnv() diff --git a/api/src/main.ts b/api/src/main.ts new file mode 100644 index 0000000..a196bb4 --- /dev/null +++ b/api/src/main.ts @@ -0,0 +1,19 @@ +import './load-env' +import { Logger } from '@nestjs/common' +import { NestFactory } from '@nestjs/core' +import { NestExpressApplication } from '@nestjs/platform-express' +import { AppModule } from './app.module' +import { configureApp } from './app.setup' + +async function bootstrap() { + const app = await NestFactory.create(AppModule) + + configureApp(app) + app.enableShutdownHooks() + const port = Number(process.env.PORT) || 3003 + + // Unset listens on all interfaces, which the container needs. HOST=127.0.0.1 keeps a local run private. + await app.listen(port, process.env.HOST || '0.0.0.0') + new Logger('Bootstrap').log(`Server running on port ${port}`) +} +bootstrap() diff --git a/api/src/merchant-rules/merchant-rules.controller.ts b/api/src/merchant-rules/merchant-rules.controller.ts new file mode 100644 index 0000000..b6e193e --- /dev/null +++ b/api/src/merchant-rules/merchant-rules.controller.ts @@ -0,0 +1,50 @@ +import { HttpCode, Body, Controller, Delete, Get, Param, Post, UseGuards } from '@nestjs/common' +import { AccessGuard, CurrentActor } from '../access/access.guard' +import { Actor } from '../access/access.settings' +import { CsrfGuard } from '../access/csrf.guard' +import { ZodValidationPipe } from '../shared/http/zod-validation.pipe' +import { MerchantRulesService } from './merchant-rules.service' +import { SaveRuleInput, SaveRuleSchema, SaveRulesInput, SaveRulesSchema } from './merchant-rules.schemas' + +@Controller('api/learn') +@UseGuards(AccessGuard, CsrfGuard) +export class MerchantRulesController { + constructor(private readonly service: MerchantRulesService) {} + + @Get('mappings') + list(@CurrentActor() actor: Actor) { + return { success: true, data: this.service.list(actor.userId) } + } + + @Get('stats') + stats(@CurrentActor() actor: Actor) { + return { success: true, data: this.service.stats(actor.userId) } + } + + @Post('mapping') + @HttpCode(200) // the previous API answered 200, not 201 + learn(@CurrentActor() actor: Actor, @Body(new ZodValidationPipe(SaveRuleSchema)) body: SaveRuleInput) { + this.service.learn(actor.userId, body.merchant, body.category) + + return { + success: true, + message: `Learned: "${body.merchant}" → "${body.category}"`, + } + } + + @Post('mappings/batch') + @HttpCode(200) // the previous API answered 200, not 201 + learnMany(@CurrentActor() actor: Actor, @Body(new ZodValidationPipe(SaveRulesSchema)) body: SaveRulesInput) { + this.service.learnMany(actor.userId, body.mappings) + + return { success: true, message: `Saved ${body.mappings.length} mappings` } + } + + @Delete('mapping/:merchant') + forget(@CurrentActor() actor: Actor, @Param('merchant') merchant: string) { + // Express has already decoded the path segment + this.service.forget(actor.userId, merchant) + + return { success: true, message: `Deleted mapping for "${merchant}"` } + } +} diff --git a/api/src/merchant-rules/merchant-rules.module.ts b/api/src/merchant-rules/merchant-rules.module.ts new file mode 100644 index 0000000..593e6b0 --- /dev/null +++ b/api/src/merchant-rules/merchant-rules.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common' +import { MerchantRulesController } from './merchant-rules.controller' +import { MerchantRulesRepository } from './merchant-rules.repository' +import { MerchantRulesService } from './merchant-rules.service' + +@Module({ + controllers: [MerchantRulesController], + providers: [MerchantRulesService, MerchantRulesRepository], + exports: [MerchantRulesService], +}) +export class MerchantRulesModule {} diff --git a/api/src/merchant-rules/merchant-rules.repository.ts b/api/src/merchant-rules/merchant-rules.repository.ts new file mode 100644 index 0000000..24c51b3 --- /dev/null +++ b/api/src/merchant-rules/merchant-rules.repository.ts @@ -0,0 +1,139 @@ +import { Injectable } from '@nestjs/common' +import { DatabaseService } from '../shared/database/database.service' +import { hasColumn } from '../shared/database/migrate' +import { UsersService } from '../users/users.service' + +interface MerchantRule { + merchant: string + merchantDisplay?: string + category: string + confidence: number +} + +// Frontend also has this normalization function. They MUST match for consistent storage and retrieval. +function normalizeMerchant(name: string): string { + return name + .toLowerCase() + .trim() + .replace(/[^a-z0-9\s]/g, '') + .replace(/\s+/g, ' ') + .trim() +} + +@Injectable() +export class MerchantRulesRepository { + private readonly upsert + private readonly all + private readonly remove + + constructor( + private readonly database: DatabaseService, + users: UsersService, + ) { + const { db } = database + + // Older databases had one global list. Rebuild it per user; the old rows wait for their owner. + if (db.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name='merchant_categories'").get()) { + if (!hasColumn(db, 'merchant_categories', 'user_id')) { + db.exec(` + ALTER TABLE merchant_categories RENAME TO merchant_categories_old; + DROP INDEX IF EXISTS idx_merchant_key; + DROP INDEX IF EXISTS idx_category; + `) + this.createTable() + db.exec(` + INSERT INTO merchant_categories (user_id, merchant_key, merchant_display, category, confidence, created_at, updated_at) + SELECT NULL, merchant_key, merchant_display, category, confidence, created_at, updated_at FROM merchant_categories_old; + DROP TABLE merchant_categories_old; + `) + } + } else { + this.createTable() + } + users.onLegacyOwnerCreated(userId => + db.prepare('UPDATE merchant_categories SET user_id = ? WHERE user_id IS NULL').run(userId), + ) + this.upsert = db.prepare(` + INSERT INTO merchant_categories (user_id, merchant_key, merchant_display, category, updated_at) + VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP) + ON CONFLICT(user_id, merchant_key) DO UPDATE SET + category = excluded.category, confidence = confidence + 1, updated_at = CURRENT_TIMESTAMP + `) + this.all = db.prepare(` + SELECT merchant_key as merchant, merchant_display, category, confidence + FROM merchant_categories WHERE user_id = ? ORDER BY confidence DESC, updated_at DESC + `) + this.remove = db.prepare('DELETE FROM merchant_categories WHERE user_id = ? AND merchant_key = ?') + } + + private createTable() { + this.database.db.exec(` + CREATE TABLE IF NOT EXISTS merchant_categories ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id TEXT, + merchant_key TEXT NOT NULL, + merchant_display TEXT NOT NULL, + category TEXT NOT NULL, + confidence INTEGER DEFAULT 1, + created_at TEXT DEFAULT CURRENT_TIMESTAMP, + updated_at TEXT DEFAULT CURRENT_TIMESTAMP, + UNIQUE (user_id, merchant_key) + ); + CREATE INDEX IF NOT EXISTS idx_merchant_user ON merchant_categories(user_id, merchant_key); + `) + } + + save(userId: string, merchant: string, category: string): void { + const key = normalizeMerchant(merchant) + + if (!key || key.length < 2) { + return + } // skip empty/short merchants + this.upsert.run(userId, key, merchant, category) + } + + saveMany(userId: string, rules: Array<{ merchant: string; category: string }>): void { + this.database.db.transaction((items: typeof rules) => + items.forEach(r => this.save(userId, r.merchant, r.category)), + )(rules) + } + + findAll(userId: string): MerchantRule[] { + return ( + this.all.all(userId) as Array<{ + merchant: string + merchant_display: string + category: string + confidence: number + }> + ).map(r => ({ + merchant: r.merchant, + merchantDisplay: r.merchant_display, + category: r.category, + confidence: r.confidence, + })) + } + + delete(userId: string, merchant: string): void { + this.remove.run(userId, normalizeMerchant(merchant)) + } + + stats(userId: string) { + const { db } = this.database + const total = ( + db.prepare('SELECT COUNT(*) as count FROM merchant_categories WHERE user_id = ?').get(userId) as { count: number } + ).count + const topCategories = db + .prepare( + 'SELECT category, COUNT(*) as count FROM merchant_categories WHERE user_id = ? GROUP BY category ORDER BY count DESC LIMIT 10', + ) + .all(userId) as Array<{ category: string; count: number }> + const recentMappings = db + .prepare( + 'SELECT merchant_key as merchant, merchant_display, category, confidence FROM merchant_categories WHERE user_id = ? ORDER BY updated_at DESC LIMIT 10', + ) + .all(userId) + + return { total, topCategories, recentMappings } + } +} diff --git a/api/src/merchant-rules/merchant-rules.schemas.ts b/api/src/merchant-rules/merchant-rules.schemas.ts new file mode 100644 index 0000000..a2c436d --- /dev/null +++ b/api/src/merchant-rules/merchant-rules.schemas.ts @@ -0,0 +1,9 @@ +import { z } from 'zod' + +export const SaveRuleSchema = z.object({ + merchant: z.string().min(1), + category: z.string().min(1), +}) +export const SaveRulesSchema = z.object({ mappings: z.array(SaveRuleSchema) }) +export type SaveRuleInput = z.infer +export type SaveRulesInput = z.infer diff --git a/api/src/merchant-rules/merchant-rules.service.ts b/api/src/merchant-rules/merchant-rules.service.ts new file mode 100644 index 0000000..00f26d5 --- /dev/null +++ b/api/src/merchant-rules/merchant-rules.service.ts @@ -0,0 +1,31 @@ +import { Injectable } from '@nestjs/common' +import { MerchantRulesRepository } from './merchant-rules.repository' + +/** + * Rules the app learns when you recategorize a merchant ("DeepSeek is Subscriptions"). + * The web app loads them all and does its own fuzzy matching. + */ +@Injectable() +export class MerchantRulesService { + constructor(private readonly rules: MerchantRulesRepository) {} + + list(userId: string) { + return this.rules.findAll(userId) + } + + stats(userId: string) { + return this.rules.stats(userId) + } + + learn(userId: string, merchant: string, category: string) { + this.rules.save(userId, merchant, category) + } + + learnMany(userId: string, rules: Array<{ merchant: string; category: string }>) { + this.rules.saveMany(userId, rules) + } + + forget(userId: string, merchant: string) { + this.rules.delete(userId, merchant) + } +} diff --git a/api/src/routes/categorize.ts b/api/src/routes/categorize.ts deleted file mode 100644 index c6c4eeb..0000000 --- a/api/src/routes/categorize.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { Hono } from 'hono' -import { z } from 'zod' -import { categorizeWithAI } from '../services/openai' -import { validateAnonymized } from '../lib/anonymizer' - -const categorizeRoute = new Hono() - -const TransactionSchema = z.object({ - id: z.string(), - description: z.string(), - amount: z.number(), - date: z.string(), - category: z.string().optional(), -}) - -const CategorizeRequestSchema = z.object({ - transactions: z.array(TransactionSchema), -}) - -categorizeRoute.post('/', async (c) => { - try { - const body = await c.req.json() - const parsed = CategorizeRequestSchema.safeParse(body) - - if (!parsed.success) { - return c.json({ error: 'Invalid request body', details: parsed.error.issues }, 400) - } - - const { transactions } = parsed.data - - // Validate that data is anonymized before processing - const anonymizationCheck = validateAnonymized(transactions) - if (!anonymizationCheck.isValid) { - return c.json({ - error: 'Data contains non-anonymized personal information', - details: anonymizationCheck.issues - }, 400) - } - - // Call OpenAI for categorization - const categorized = await categorizeWithAI(transactions) - - return c.json({ - success: true, - data: categorized - }) - } catch (error) { - console.error('Categorization error:', error) - return c.json({ error: 'Failed to categorize transactions' }, 500) - } -}) - -export { categorizeRoute } diff --git a/api/src/routes/export.ts b/api/src/routes/export.ts deleted file mode 100644 index 5935989..0000000 --- a/api/src/routes/export.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { Hono } from 'hono' -import { z } from 'zod' -import { exportToSheets, getSheetLayout, getSheetMetadata } from '../services/sheets' - -const exportRoute = new Hono() - -const CategoryValueSchema = z.object({ - column: z.string(), - value: z.number(), -}) - -const ExportRequestSchema = z.object({ - sheetId: z.string(), - tabName: z.string(), - month: z.string(), - monthFormat: z.enum(['short', 'long', 'numeric']), - categoryValues: z.array(CategoryValueSchema), - income: z.number(), - savings: z.number().optional(), - investment: z.number().optional(), - constantColumns: z.array(z.string()).optional(), - incomeColumn: z.string().optional(), - savingsColumn: z.string().optional(), - investmentColumn: z.string().optional(), -}) - -// Export data to Google Sheets -exportRoute.post('/sheets', async (c) => { - try { - const body = await c.req.json() - const parsed = ExportRequestSchema.safeParse(body) - - if (!parsed.success) { - return c.json({ error: 'Invalid request body', details: parsed.error.issues }, 400) - } - - const result = await exportToSheets(parsed.data) - - return c.json({ - success: true, - data: result - }) - } catch (error) { - console.error('Export error:', error) - const message = error instanceof Error ? error.message : 'Failed to export to Google Sheets' - return c.json({ error: message }, 500) - } -}) - -// Read the sheet's header row so the app mirrors the real column layout -exportRoute.post('/sheets/layout', async (c) => { - try { - const body = await c.req.json() - const { sheetId, tabName } = body - - if (!sheetId || !tabName) { - return c.json({ error: 'Missing sheetId or tabName' }, 400) - } - - const result = await getSheetLayout(sheetId, tabName) - - return c.json({ success: true, data: result }) - } catch (error) { - console.error('Layout error:', error) - const message = error instanceof Error ? error.message : 'Failed to read sheet layout' - return c.json({ error: message }, 500) - } -}) - -// Get sheet metadata (available tabs) -exportRoute.post('/sheets/metadata', async (c) => { - try { - const body = await c.req.json() - const { sheetId } = body - - if (!sheetId) { - return c.json({ error: 'Missing sheetId' }, 400) - } - - const result = await getSheetMetadata(sheetId) - - return c.json({ - success: true, - data: result - }) - } catch (error) { - console.error('Metadata error:', error) - const message = error instanceof Error ? error.message : 'Failed to fetch sheet metadata' - return c.json({ error: message }, 500) - } -}) - -export { exportRoute } diff --git a/api/src/routes/learn.ts b/api/src/routes/learn.ts deleted file mode 100644 index 1481710..0000000 --- a/api/src/routes/learn.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { Hono } from 'hono' -import { z } from 'zod' -import { - saveMerchantCategory, - getAllMerchantMappings, - deleteMerchantMapping, - saveBatchMappings, - getMappingStats, -} from '../services/database' - -const learnRoute = new Hono() - -// Schema for saving a single mapping -const SaveMappingSchema = z.object({ - merchant: z.string().min(1), - category: z.string().min(1), -}) - -// Schema for batch save -const BatchSaveSchema = z.object({ - mappings: z.array(SaveMappingSchema), -}) - -// Get all learned mappings -learnRoute.get('/mappings', (c) => { - try { - const mappings = getAllMerchantMappings() - return c.json({ - success: true, - data: mappings, - }) - } catch (error) { - console.error('Failed to get mappings:', error) - return c.json({ error: 'Failed to get mappings' }, 500) - } -}) - -// Get stats about learned mappings -learnRoute.get('/stats', (c) => { - try { - const stats = getMappingStats() - return c.json({ - success: true, - data: stats, - }) - } catch (error) { - console.error('Failed to get stats:', error) - return c.json({ error: 'Failed to get stats' }, 500) - } -}) - -// Save a single merchant → category mapping -learnRoute.post('/mapping', async (c) => { - try { - const body = await c.req.json() - const parsed = SaveMappingSchema.safeParse(body) - - if (!parsed.success) { - return c.json({ error: 'Invalid request', details: parsed.error.issues }, 400) - } - - const { merchant, category } = parsed.data - saveMerchantCategory(merchant, category) - - return c.json({ - success: true, - message: `Learned: "${merchant}" → "${category}"`, - }) - } catch (error) { - console.error('Failed to save mapping:', error) - return c.json({ error: 'Failed to save mapping' }, 500) - } -}) - -// Save multiple mappings at once -learnRoute.post('/mappings/batch', async (c) => { - try { - const body = await c.req.json() - const parsed = BatchSaveSchema.safeParse(body) - - if (!parsed.success) { - return c.json({ error: 'Invalid request', details: parsed.error.issues }, 400) - } - - saveBatchMappings(parsed.data.mappings) - - return c.json({ - success: true, - message: `Saved ${parsed.data.mappings.length} mappings`, - }) - } catch (error) { - console.error('Failed to save batch mappings:', error) - return c.json({ error: 'Failed to save batch mappings' }, 500) - } -}) - -// Delete a mapping -learnRoute.delete('/mapping/:merchant', (c) => { - try { - const merchant = decodeURIComponent(c.req.param('merchant')) - deleteMerchantMapping(merchant) - - return c.json({ - success: true, - message: `Deleted mapping for "${merchant}"`, - }) - } catch (error) { - console.error('Failed to delete mapping:', error) - return c.json({ error: 'Failed to delete mapping' }, 500) - } -}) - -export { learnRoute } diff --git a/api/src/routes/settings.ts b/api/src/routes/settings.ts deleted file mode 100644 index acfb11e..0000000 --- a/api/src/routes/settings.ts +++ /dev/null @@ -1,138 +0,0 @@ -import { Hono } from 'hono' -import { z } from 'zod' -import { - getSettingValue, - setSettingValue, - getAllSettingsMap, -} from '../services/database' - -const settingsRoute = new Hono() - -// ============ Schema with Defaults ============ - -const CategoryMappingSchema = z.object({ - appCategory: z.string(), - sheetColumn: z.string(), -}) - -// Column layout read from the sheet's header row (source of truth when present) -const SheetColumnSchema = z.object({ - column: z.string(), - name: z.string(), - kind: z.enum(['month', 'category', 'total', 'income', 'incomeAfter', 'savings', 'investment', 'net', 'other']), -}) - -// Default category mappings matching your Google Sheet structure -const DEFAULT_CATEGORY_MAPPINGS = [ - { appCategory: 'Rent', sheetColumn: 'B' }, - { appCategory: 'Eating Out', sheetColumn: 'C' }, - { appCategory: 'Personal Entertainment', sheetColumn: 'D' }, - { appCategory: 'Subscriptions', sheetColumn: 'E' }, - { appCategory: 'Car', sheetColumn: 'F' }, - { appCategory: 'Public Transport', sheetColumn: 'G' }, - { appCategory: 'Internet', sheetColumn: 'H' }, - { appCategory: 'Electricity', sheetColumn: 'I' }, - { appCategory: 'Insurance', sheetColumn: 'J' }, - { appCategory: 'Travel', sheetColumn: 'K' }, - { appCategory: 'Groceries', sheetColumn: 'L' }, - { appCategory: 'Family', sheetColumn: 'M' }, - { appCategory: 'Radio Tax', sheetColumn: 'N' }, - { appCategory: 'Health & Wellbeing', sheetColumn: 'O' }, - { appCategory: 'Shopping', sheetColumn: 'P' }, - { appCategory: 'Other', sheetColumn: 'Q' }, - { appCategory: 'Gifts', sheetColumn: 'R' }, -] - -// Single source of truth: Schema with embedded defaults -const SheetConfigSchema = z.object({ - sheetId: z.string().default(''), - tabName: z.string().default('2026'), - monthColumn: z.string().default('A'), - monthFormat: z.enum(['short', 'long', 'numeric']).default('short'), - categoryMappings: z.array(CategoryMappingSchema).default(DEFAULT_CATEGORY_MAPPINGS), - // Formula columns (auto-calculated, don't overwrite) - constantColumns: z.array(z.string()).default(['S', 'U', 'X']), - // Data columns we write to - incomeColumn: z.string().default('T'), - savingsColumn: z.string().default('V'), - investmentColumn: z.string().default('W'), - // Synced from the sheet; undefined until the first sync - layout: z.array(SheetColumnSchema).optional(), - headerRow: z.number().optional(), - layoutSyncedAt: z.number().optional(), -}) - -// Infer the type from schema -type SheetConfig = z.infer - -// Generate defaults by parsing empty object -const getDefaultConfig = (): SheetConfig => SheetConfigSchema.parse({}) - -// ============ Routes ============ - -// Get sheet config (merges stored values with defaults) -settingsRoute.get('/sheet', (c) => { - const stored = getSettingValue>('sheetConfig', {}) - const config = SheetConfigSchema.parse(stored) // Applies defaults for missing fields - - return c.json({ success: true, data: config }) -}) - -// Save sheet config -settingsRoute.post('/sheet', async (c) => { - try { - const body = await c.req.json() - const config = SheetConfigSchema.parse(body) - - setSettingValue('sheetConfig', config) - - return c.json({ success: true, message: 'Settings saved' }) - } catch (error) { - if (error instanceof z.ZodError) { - return c.json({ success: false, error: error.issues }, 400) - } - return c.json({ success: false, error: 'Invalid request' }, 400) - } -}) - -// Reset to defaults -settingsRoute.delete('/sheet', (c) => { - setSettingValue('sheetConfig', getDefaultConfig()) - return c.json({ success: true, message: 'Reset to defaults' }) -}) - -// Get all settings -settingsRoute.get('/', (c) => { - const stored = getAllSettingsMap() - - // Apply defaults to sheetConfig if present - const sheetConfig = stored.sheetConfig - ? SheetConfigSchema.parse(stored.sheetConfig) - : getDefaultConfig() - - return c.json({ - success: true, - data: { ...stored, sheetConfig }, - }) -}) - -// Generic key-value get/set -settingsRoute.get('/:key', (c) => { - const key = c.req.param('key') - const value = getSettingValue(key, null) - - if (value === null) { - return c.json({ success: false, error: 'Not found' }, 404) - } - return c.json({ success: true, data: value }) -}) - -settingsRoute.post('/:key', async (c) => { - const key = c.req.param('key') - const body = await c.req.json() - - setSettingValue(key, body.value ?? body) - return c.json({ success: true }) -}) - -export { settingsRoute } diff --git a/api/src/services/database.ts b/api/src/services/database.ts deleted file mode 100644 index 7845743..0000000 --- a/api/src/services/database.ts +++ /dev/null @@ -1,239 +0,0 @@ -import Database from 'better-sqlite3' -import { join, dirname } from 'path' -import { fileURLToPath } from 'url' -import { mkdirSync, existsSync } from 'fs' - -const __filename = fileURLToPath(import.meta.url) -const __dirname = dirname(__filename) - -// Data directory for persistent storage -const DATA_DIR = process.env.DATA_DIR || join(__dirname, '../../data') -const DB_PATH = join(DATA_DIR, 'categorizations.db') - -// Ensure data directory exists -if (!existsSync(DATA_DIR)) { - mkdirSync(DATA_DIR, { recursive: true }) -} - -// Initialize database -const db = new Database(DB_PATH) - -// Enable WAL mode for better concurrent read/write performance -db.pragma('journal_mode = WAL') -db.pragma('synchronous = NORMAL') -db.pragma('cache_size = 10000') - -// Create tables -db.exec(` - -- Merchant category mappings - CREATE TABLE IF NOT EXISTS merchant_categories ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - merchant_key TEXT NOT NULL UNIQUE, - merchant_display TEXT NOT NULL, - category TEXT NOT NULL, - confidence INTEGER DEFAULT 1, - created_at TEXT DEFAULT CURRENT_TIMESTAMP, - updated_at TEXT DEFAULT CURRENT_TIMESTAMP - ); - CREATE INDEX IF NOT EXISTS idx_merchant_key ON merchant_categories(merchant_key); - CREATE INDEX IF NOT EXISTS idx_category ON merchant_categories(category); - - -- App settings (key-value store) - CREATE TABLE IF NOT EXISTS settings ( - key TEXT PRIMARY KEY, - value TEXT NOT NULL, - updated_at TEXT DEFAULT CURRENT_TIMESTAMP - ); -`) - -// ============ Normalization ============ -// Note: Frontend also has this same normalization function -// They MUST match for consistent storage and retrieval - -function normalizeMerchant(name: string): string { - return name - .toLowerCase() - .trim() - .replace(/[^a-z0-9\s]/g, '') // Remove special chars - .replace(/\s+/g, ' ') // Multiple spaces → single space - .trim() -} - -// ============ Prepared Statements ============ - -const insertOrUpdateMapping = db.prepare(` - INSERT INTO merchant_categories (merchant_key, merchant_display, category, updated_at) - VALUES (?, ?, ?, CURRENT_TIMESTAMP) - ON CONFLICT(merchant_key) DO UPDATE SET - category = excluded.category, - confidence = confidence + 1, - updated_at = CURRENT_TIMESTAMP -`) - -const getAllMappings = db.prepare(` - SELECT merchant_key as merchant, merchant_display, category, confidence - FROM merchant_categories - ORDER BY confidence DESC, updated_at DESC -`) - -const getMapping = db.prepare(` - SELECT category, confidence, merchant_display - FROM merchant_categories - WHERE merchant_key = ? -`) - -const deleteMapping = db.prepare(` - DELETE FROM merchant_categories - WHERE merchant_key = ? -`) - -// ============ Export Types ============ - -export interface MerchantMapping { - merchant: string - merchantDisplay?: string - category: string - confidence: number -} - -// ============ Core Functions ============ - -/** - * Save a merchant → category mapping - * - Normalizes merchant name for deduplication - * - Uses UPSERT: inserts new or increments confidence if exists - */ -export function saveMerchantCategory(merchant: string, category: string): void { - const key = normalizeMerchant(merchant) - if (!key || key.length < 2) return // Skip empty/short merchants - - insertOrUpdateMapping.run(key, merchant, category) -} - -/** - * Get category for a specific merchant (exact normalized match) - * Note: Fuzzy matching is handled by frontend - */ -export function getMerchantCategory(merchant: string): string | null { - const key = normalizeMerchant(merchant) - const result = getMapping.get(key) as { category: string } | undefined - return result?.category ?? null -} - -/** - * Get all merchant → category mappings - * Frontend loads these once and does its own fuzzy matching - */ -export function getAllMerchantMappings(): MerchantMapping[] { - const results = getAllMappings.all() as Array<{ - merchant: string - merchant_display: string - category: string - confidence: number - }> - - return results.map(r => ({ - merchant: r.merchant, - merchantDisplay: r.merchant_display, - category: r.category, - confidence: r.confidence, - })) -} - -/** - * Delete a merchant mapping - */ -export function deleteMerchantMapping(merchant: string): void { - const key = normalizeMerchant(merchant) - deleteMapping.run(key) -} - -/** - * Batch save mappings (wrapped in transaction for efficiency) - */ -export function saveBatchMappings(mappings: Array<{ merchant: string; category: string }>): void { - const transaction = db.transaction((items: typeof mappings) => { - for (const { merchant, category } of items) { - saveMerchantCategory(merchant, category) - } - }) - transaction(mappings) -} - -// ============ Stats ============ - -export function getMappingStats(): { - total: number - topCategories: Array<{ category: string; count: number }> - recentMappings: MerchantMapping[] -} { - const total = (db.prepare('SELECT COUNT(*) as count FROM merchant_categories').get() as { count: number }).count - - const topCategories = db.prepare(` - SELECT category, COUNT(*) as count - FROM merchant_categories - GROUP BY category - ORDER BY count DESC - LIMIT 10 - `).all() as Array<{ category: string; count: number }> - - const recentMappings = db.prepare(` - SELECT merchant_key as merchant, merchant_display, category, confidence - FROM merchant_categories - ORDER BY updated_at DESC - LIMIT 10 - `).all() as MerchantMapping[] - - return { total, topCategories, recentMappings } -} - -// ============ Settings ============ - -const getSetting = db.prepare('SELECT value FROM settings WHERE key = ?') -const setSetting = db.prepare(` - INSERT INTO settings (key, value, updated_at) VALUES (?, ?, CURRENT_TIMESTAMP) - ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = CURRENT_TIMESTAMP -`) -const deleteSetting = db.prepare('DELETE FROM settings WHERE key = ?') -const getAllSettings = db.prepare('SELECT key, value FROM settings') - -export function getSettingValue(key: string, defaultValue: T): T { - const result = getSetting.get(key) as { value: string } | undefined - if (!result) return defaultValue - try { - return JSON.parse(result.value) as T - } catch { - return defaultValue - } -} - -export function setSettingValue(key: string, value: T): void { - setSetting.run(key, JSON.stringify(value)) -} - -export function deleteSettingValue(key: string): void { - deleteSetting.run(key) -} - -export function getAllSettingsMap(): Record { - const results = getAllSettings.all() as Array<{ key: string; value: string }> - const settings: Record = {} - for (const { key, value } of results) { - try { - settings[key] = JSON.parse(value) - } catch { - settings[key] = value - } - } - return settings -} - -// ============ Maintenance ============ - -export function optimizeDatabase(): void { - db.pragma('optimize') -} - -export function closeDatabase(): void { - db.close() -} diff --git a/api/src/services/openai.ts b/api/src/services/openai.ts deleted file mode 100644 index 729d7f2..0000000 --- a/api/src/services/openai.ts +++ /dev/null @@ -1,99 +0,0 @@ -import OpenAI from 'openai' - -interface TransactionInput { - id: string; - description: string; - amount: number; - date: string; - category?: string; -} - -interface CategorizedTransaction { - id: string; - category: string; - merchant?: string; -} - -// Lazy initialization to avoid throwing on import -let openaiClient: OpenAI | null = null - -function getOpenAIClient(): OpenAI { - if (!openaiClient) { - if (!process.env.OPENAI_API_KEY) { - throw new Error('OPENAI_API_KEY environment variable is not set') - } - openaiClient = new OpenAI({ - apiKey: process.env.OPENAI_API_KEY, - }) - } - return openaiClient -} - -const SYSTEM_PROMPT = `You are a transaction categorization assistant. Given a list of bank transactions, categorize each one into the most appropriate category. - -Available categories: -- Groceries (supermarkets, food stores) -- Eating Out (restaurants, cafes, food delivery) -- Personal Entertainment (games, streaming, apps) -- Subscriptions (recurring digital services) -- Car (fuel, parking, car-related) -- Public Transport (trains, buses, metro) -- Internet (internet/phone bills) -- Electricity (utility bills) -- Insurance (all types of insurance) -- Travel (flights, hotels, vacation) -- Family (gifts to family, family expenses) -- Radio Tax (broadcasting fees) -- Health & Wellbeing (pharmacy, doctors, gym) -- Other (anything that doesn't fit above) -- Gifts (presents, donations) -- Income (salary, refunds, incoming transfers) -- Investment (stocks, ETFs, savings) -- Transfer (between own accounts - internal) - -For each transaction, return the category and extract the merchant name if possible. - -Respond in JSON format only: -[ - { "id": "...", "category": "...", "merchant": "..." }, - ... -]` - -export async function categorizeWithAI(transactions: TransactionInput[]): Promise { - const openai = getOpenAIClient() - - // Batch transactions for efficiency - const BATCH_SIZE = 25 - const results: CategorizedTransaction[] = [] - - for (let i = 0; i < transactions.length; i += BATCH_SIZE) { - const batch = transactions.slice(i, i + BATCH_SIZE) - - const userMessage = batch.map(tx => - `ID: ${tx.id}, Amount: ${tx.amount}, Date: ${tx.date}, Description: ${tx.description}` - ).join('\n') - - const response = await openai.chat.completions.create({ - model: 'gpt-4o-mini', - messages: [ - { role: 'system', content: SYSTEM_PROMPT }, - { role: 'user', content: userMessage } - ], - temperature: 0.1, - response_format: { type: 'json_object' } - }) - - const content = response.choices[0]?.message?.content - if (content) { - try { - const parsed = JSON.parse(content) - const items = Array.isArray(parsed) ? parsed : parsed.transactions || [] - results.push(...items) - } catch (e) { - console.error('Failed to parse AI response:', e) - } - } - } - - return results -} diff --git a/api/src/settings/settings.controller.ts b/api/src/settings/settings.controller.ts new file mode 100644 index 0000000..f6f371f --- /dev/null +++ b/api/src/settings/settings.controller.ts @@ -0,0 +1,66 @@ +import { HttpCode, Body, Controller, Delete, Get, NotFoundException, Param, Post, UseGuards } from '@nestjs/common' +import { AccessGuard, CurrentActor } from '../access/access.guard' +import { Actor } from '../access/access.settings' +import { CsrfGuard } from '../access/csrf.guard' +import { ZodValidationPipe } from '../shared/http/zod-validation.pipe' +import { SettingsService } from './settings.service' +import { SheetConfig, SheetConfigSchema } from './sheet-config.schema' + +@Controller('api/settings') +@UseGuards(AccessGuard, CsrfGuard) +export class SettingsController { + constructor(private readonly service: SettingsService) {} + + @Get('sheet') + sheet(@CurrentActor() actor: Actor) { + return { success: true, data: this.service.sheetConfig(actor.userId) } + } + + @Post('sheet') + @HttpCode(200) // the previous API answered 200, not 201 + saveSheet( + @CurrentActor() actor: Actor, + @Body(new ZodValidationPipe(SheetConfigSchema, { shape: 'success-false' })) + config: SheetConfig, + ) { + this.service.saveSheetConfig(actor.userId, config) + + return { success: true, message: 'Settings saved' } + } + + @Delete('sheet') + resetSheet(@CurrentActor() actor: Actor) { + this.service.resetSheetConfig(actor.userId) + + return { success: true, message: 'Reset to defaults' } + } + + @Get() + all(@CurrentActor() actor: Actor) { + return { success: true, data: this.service.everything(actor.userId) } + } + + // Generic key-value (declared after the fixed routes above) + @Get(':key') + get(@CurrentActor() actor: Actor, @Param('key') key: string) { + const value = this.service.get(actor.userId, key) + + if (value === null) { + throw new NotFoundException({ success: false, error: 'Not found' }) + } + + return { success: true, data: value } + } + + @Post(':key') + @HttpCode(200) // the previous API answered 200, not 201 + set( + @CurrentActor() actor: Actor, + @Param('key') key: string, + @Body() body: { value?: unknown } & Record, + ) { + this.service.set(actor.userId, key, body?.value ?? body) + + return { success: true } + } +} diff --git a/api/src/settings/settings.module.ts b/api/src/settings/settings.module.ts new file mode 100644 index 0000000..ac53180 --- /dev/null +++ b/api/src/settings/settings.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common' +import { SettingsController } from './settings.controller' +import { SettingsRepository } from './settings.repository' +import { SettingsService } from './settings.service' + +@Module({ + controllers: [SettingsController], + providers: [SettingsService, SettingsRepository], + exports: [SettingsService], +}) +export class SettingsModule {} diff --git a/api/src/settings/settings.repository.ts b/api/src/settings/settings.repository.ts new file mode 100644 index 0000000..72558f6 --- /dev/null +++ b/api/src/settings/settings.repository.ts @@ -0,0 +1,90 @@ +import { Injectable } from '@nestjs/common' +import { DatabaseService } from '../shared/database/database.service' +import { hasColumn } from '../shared/database/migrate' +import { UsersService } from '../users/users.service' + +/** One person's saved preferences. Every row belongs to a user. */ +@Injectable() +export class SettingsRepository { + private readonly selectOne + private readonly upsertOne + private readonly selectAll + + constructor( + private readonly database: DatabaseService, + users: UsersService, + ) { + const { db } = database + + // Older databases kept one global list. Rebuild it per user; the old rows wait for their owner. + if (db.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name='settings'").get()) { + if (!hasColumn(db, 'settings', 'user_id')) { + db.exec('ALTER TABLE settings RENAME TO settings_old') + this.createTable() + db.exec(` + INSERT INTO settings (user_id, key, value, updated_at) + SELECT NULL, key, value, updated_at FROM settings_old; + DROP TABLE settings_old; + `) + } + } else { + this.createTable() + } + users.onLegacyOwnerCreated(userId => + db.prepare('UPDATE settings SET user_id = ? WHERE user_id IS NULL').run(userId), + ) + this.selectOne = db.prepare('SELECT value FROM settings WHERE user_id = ? AND key = ?') + this.upsertOne = db.prepare(` + INSERT INTO settings (user_id, key, value, updated_at) VALUES (?, ?, ?, CURRENT_TIMESTAMP) + ON CONFLICT(user_id, key) DO UPDATE SET value = excluded.value, updated_at = CURRENT_TIMESTAMP + `) + this.selectAll = db.prepare('SELECT key, value FROM settings WHERE user_id = ?') + } + + private createTable() { + this.database.db.exec(` + CREATE TABLE IF NOT EXISTS settings ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id TEXT, + key TEXT NOT NULL, + value TEXT NOT NULL, + updated_at TEXT DEFAULT CURRENT_TIMESTAMP, + UNIQUE (user_id, key) + ); + `) + } + + read(userId: string, key: string, fallback: T): T { + const row = this.selectOne.get(userId, key) as { value: string } | undefined + + if (!row) { + return fallback + } + try { + return JSON.parse(row.value) as T + } catch { + return fallback + } + } + + write(userId: string, key: string, value: T): void { + this.upsertOne.run(userId, key, JSON.stringify(value)) + } + + readAll(userId: string): Record { + const out: Record = {} + + for (const { key, value } of this.selectAll.all(userId) as Array<{ + key: string + value: string + }>) { + try { + out[key] = JSON.parse(value) + } catch { + out[key] = value + } + } + + return out + } +} diff --git a/api/src/settings/settings.service.ts b/api/src/settings/settings.service.ts new file mode 100644 index 0000000..9b1c875 --- /dev/null +++ b/api/src/settings/settings.service.ts @@ -0,0 +1,38 @@ +import { Injectable } from '@nestjs/common' +import { SettingsRepository } from './settings.repository' +import { SheetConfig, SheetConfigSchema, defaultSheetConfig } from './sheet-config.schema' + +/** Preferences that need to follow you across browsers: mainly how your Google Sheet is laid out. */ +@Injectable() +export class SettingsService { + constructor(private readonly settings: SettingsRepository) {} + + sheetConfig(userId: string): SheetConfig { + return SheetConfigSchema.parse(this.settings.read>(userId, 'sheetConfig', {})) // fills in defaults + } + + saveSheetConfig(userId: string, config: SheetConfig) { + this.settings.write(userId, 'sheetConfig', config) + } + + resetSheetConfig(userId: string) { + this.settings.write(userId, 'sheetConfig', defaultSheetConfig()) + } + + everything(userId: string) { + const stored = this.settings.readAll(userId) + + return { + ...stored, + sheetConfig: stored.sheetConfig ? SheetConfigSchema.parse(stored.sheetConfig) : defaultSheetConfig(), + } + } + + get(userId: string, key: string): T | null { + return this.settings.read(userId, key, null) + } + + set(userId: string, key: string, value: unknown) { + this.settings.write(userId, key, value) + } +} diff --git a/api/src/settings/sheet-config.schema.ts b/api/src/settings/sheet-config.schema.ts new file mode 100644 index 0000000..ed370a8 --- /dev/null +++ b/api/src/settings/sheet-config.schema.ts @@ -0,0 +1,52 @@ +import { z } from 'zod' + +const CategoryMappingSchema = z.object({ + appCategory: z.string(), + sheetColumn: z.string(), +}) + +// One column of the Google Sheet's header row, read from the sheet (or pasted by the user) +const SheetColumnSchema = z.object({ + column: z.string(), + name: z.string(), + kind: z.enum(['month', 'category', 'total', 'income', 'incomeAfter', 'savings', 'investment', 'net', 'other']), +}) + +// Default category mappings matching the original sheet structure +const DEFAULT_CATEGORY_MAPPINGS = [ + { appCategory: 'Rent', sheetColumn: 'B' }, + { appCategory: 'Eating Out', sheetColumn: 'C' }, + { appCategory: 'Personal Entertainment', sheetColumn: 'D' }, + { appCategory: 'Subscriptions', sheetColumn: 'E' }, + { appCategory: 'Car', sheetColumn: 'F' }, + { appCategory: 'Public Transport', sheetColumn: 'G' }, + { appCategory: 'Internet', sheetColumn: 'H' }, + { appCategory: 'Electricity', sheetColumn: 'I' }, + { appCategory: 'Insurance', sheetColumn: 'J' }, + { appCategory: 'Travel', sheetColumn: 'K' }, + { appCategory: 'Groceries', sheetColumn: 'L' }, + { appCategory: 'Family', sheetColumn: 'M' }, + { appCategory: 'Radio Tax', sheetColumn: 'N' }, + { appCategory: 'Health & Wellbeing', sheetColumn: 'O' }, + { appCategory: 'Shopping', sheetColumn: 'P' }, + { appCategory: 'Other', sheetColumn: 'Q' }, + { appCategory: 'Gifts', sheetColumn: 'R' }, +] + +// Single source of truth: the schema carries the defaults +export const SheetConfigSchema = z.object({ + sheetId: z.string().default(''), + tabName: z.string().default('2026'), + monthColumn: z.string().default('A'), + monthFormat: z.enum(['short', 'long', 'numeric']).default('short'), + categoryMappings: z.array(CategoryMappingSchema).default(DEFAULT_CATEGORY_MAPPINGS), + constantColumns: z.array(z.string()).default(['S', 'U', 'X']), // formula columns: don't overwrite + incomeColumn: z.string().default('T'), + savingsColumn: z.string().default('V'), + investmentColumn: z.string().default('W'), + layout: z.array(SheetColumnSchema).optional(), // synced from the sheet; undefined until first sync + headerRow: z.number().optional(), + layoutSyncedAt: z.number().optional(), +}) +export type SheetConfig = z.infer +export const defaultSheetConfig = (): SheetConfig => SheetConfigSchema.parse({}) diff --git a/api/src/shared/constants/time.ts b/api/src/shared/constants/time.ts new file mode 100644 index 0000000..18cc392 --- /dev/null +++ b/api/src/shared/constants/time.ts @@ -0,0 +1,28 @@ +export const MS_PER_SECOND = 1_000 +export const MS_PER_MINUTE = 60 * MS_PER_SECOND +export const MS_PER_HOUR = 60 * MS_PER_MINUTE +export const MS_PER_DAY = 24 * MS_PER_HOUR + +export const SECONDS_PER_MINUTE = 60 +export const SECONDS_PER_HOUR = 60 * SECONDS_PER_MINUTE +export const SECONDS_PER_DAY = 24 * SECONDS_PER_HOUR + +export function addDays(date: Date, days: number): Date { + return new Date(date.getTime() + days * MS_PER_DAY) +} + +export function addMilliseconds(date: Date, ms: number): Date { + return new Date(date.getTime() + ms) +} + +export function diffDays(later: Date, earlier: Date): number { + return (later.getTime() - earlier.getTime()) / MS_PER_DAY +} + +export function isoDaysFromNow(days: number, at = Date.now()): string { + return new Date(at + days * MS_PER_DAY).toISOString() +} + +export function isoDaysAgo(days: number, at = Date.now()): string { + return new Date(at - days * MS_PER_DAY).toISOString() +} diff --git a/api/src/shared/crypto/encryption.test.ts b/api/src/shared/crypto/encryption.test.ts new file mode 100644 index 0000000..e66ea7e --- /dev/null +++ b/api/src/shared/crypto/encryption.test.ts @@ -0,0 +1,22 @@ +import { describe, it, expect } from 'vitest' +import { decrypt, encrypt } from './encryption' + +const KEY = Buffer.alloc(32, 9) + +describe('crypto', () => { + it('round-trips and uses a fresh IV each time', () => { + const a = encrypt('secret', KEY), + b = encrypt('secret', KEY) + + expect(decrypt(a, KEY)).toBe('secret') + expect(a).not.toBe(b) + }) + it('rejects tampered or wrongly keyed data', () => { + const blob = encrypt('secret', KEY) + const parts = blob.split('.') + + parts[3] = Buffer.from('tampered').toString('base64') + expect(() => decrypt(parts.join('.'), KEY)).toThrow(/authenticate/i) + expect(() => decrypt(blob, Buffer.alloc(32, 1))).toThrow(/authenticate/i) + }) +}) diff --git a/api/src/shared/crypto/encryption.ts b/api/src/shared/crypto/encryption.ts new file mode 100644 index 0000000..e49fe4b --- /dev/null +++ b/api/src/shared/crypto/encryption.ts @@ -0,0 +1,32 @@ +import { createCipheriv, createDecipheriv, randomBytes, createHash } from 'node:crypto' + +// Bank session ids are credentials, so they're encrypted at rest (AES-256-GCM, random IV per value). +export function encrypt(plain: string, key: Buffer): string { + const iv = randomBytes(12) + const cipher = createCipheriv('aes-256-gcm', key, iv) + const ct = Buffer.concat([cipher.update(plain, 'utf8'), cipher.final()]) + + return ['v1', iv.toString('base64'), cipher.getAuthTag().toString('base64'), ct.toString('base64')].join('.') +} + +export function decrypt(blob: string, key: Buffer): string { + const [v, iv, tag, ct] = blob.split('.') + + if (v !== 'v1' || !iv || !tag || !ct) { + throw new Error('Unsupported ciphertext') + } + const decipher = createDecipheriv('aes-256-gcm', key, Buffer.from(iv, 'base64')) + + decipher.setAuthTag(Buffer.from(tag, 'base64')) + + return Buffer.concat([decipher.update(Buffer.from(ct, 'base64')), decipher.final()]).toString('utf8') +} + +export function randomToken(bytes = 32): string { + return randomBytes(bytes).toString('base64url') +} + +/** A short, opaque id for an account, safe to put in URLs: derived from the bank's id, which stays private. */ +export function accountRef(stableId: string): string { + return createHash('sha256').update(`finkit-account:${stableId}`).digest('hex').slice(0, 16) +} diff --git a/api/src/shared/database/database.module.ts b/api/src/shared/database/database.module.ts new file mode 100644 index 0000000..5ba6b11 --- /dev/null +++ b/api/src/shared/database/database.module.ts @@ -0,0 +1,15 @@ +import { DynamicModule, Global, Module } from '@nestjs/common' +import { DATABASE_FILE, DatabaseService } from './database.service' + +@Global() +@Module({}) +export class DatabaseModule { + /** `file` overrides where the database lives (tests pass ':memory:'). */ + static forRoot(file?: string): DynamicModule { + return { + module: DatabaseModule, + providers: [{ provide: DATABASE_FILE, useValue: file }, DatabaseService], + exports: [DatabaseService], + } + } +} diff --git a/api/src/shared/database/database.service.ts b/api/src/shared/database/database.service.ts new file mode 100644 index 0000000..48a9ca8 --- /dev/null +++ b/api/src/shared/database/database.service.ts @@ -0,0 +1,32 @@ +import { Inject, Injectable, OnModuleDestroy, Optional } from '@nestjs/common' +import Database from 'better-sqlite3' +import { existsSync, mkdirSync } from 'node:fs' +import { dirname, join } from 'node:path' + +/** Overrides the database file (tests pass ':memory:'). */ +export const DATABASE_FILE = Symbol('DATABASE_FILE') + +/** + * The single SQLite connection. Feature modules own their tables and queries; this only + * opens the database. Data lives in DATA_DIR (a mounted volume in production). + */ +@Injectable() +export class DatabaseService implements OnModuleDestroy { + readonly db: Database.Database + + constructor(@Optional() @Inject(DATABASE_FILE) file?: string) { + const path = file ?? join(process.env.DATA_DIR || join(process.cwd(), 'data'), 'categorizations.db') + + if (path !== ':memory:' && !existsSync(dirname(path))) { + mkdirSync(dirname(path), { recursive: true }) + } + this.db = new Database(path) + this.db.pragma('journal_mode = WAL') + this.db.pragma('synchronous = NORMAL') + this.db.pragma('cache_size = 10000') + } + + onModuleDestroy() { + this.db.close() + } +} diff --git a/api/src/shared/database/migrate.ts b/api/src/shared/database/migrate.ts new file mode 100644 index 0000000..f5af0c7 --- /dev/null +++ b/api/src/shared/database/migrate.ts @@ -0,0 +1,12 @@ +import type Database from 'better-sqlite3' + +export function hasColumn(db: Database.Database, table: string, column: string): boolean { + return (db.prepare(`PRAGMA table_info(${table})`).all() as Array<{ name: string }>).some(c => c.name === column) +} + +/** Adds a column to an existing table when an older database doesn't have it yet. */ +export function ensureColumn(db: Database.Database, table: string, column: string, definition: string) { + if (!hasColumn(db, table, column)) { + db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${definition}`) + } +} diff --git a/api/src/shared/http/http-error.filter.ts b/api/src/shared/http/http-error.filter.ts new file mode 100644 index 0000000..1a66609 --- /dev/null +++ b/api/src/shared/http/http-error.filter.ts @@ -0,0 +1,42 @@ +import { ArgumentsHost, Catch, ExceptionFilter, HttpException, Logger } from '@nestjs/common' +import type { Response } from 'express' + +const TEXT: Record = { + 400: 'Bad Request', + 401: 'Unauthorized', + 403: 'Forbidden', + 404: 'Not Found', + 413: 'Payload Too Large', + 429: 'Too Many Requests', +} + +/** + * Keeps error bodies in the shape the web app already reads ({ error: "…" }) and never leaks + * internals: anything unexpected becomes a plain 500. + */ +@Catch() +export class HttpErrorFilter implements ExceptionFilter { + private readonly logger = new Logger('HttpError') + + catch(exception: unknown, host: ArgumentsHost) { + const res = host.switchToHttp().getResponse() + + if (exception instanceof HttpException) { + const status = exception.getStatus() + const body = exception.getResponse() + // Nest's own errors look like { statusCode, message, error }; ours are already final + const ours = typeof body === 'object' && body !== null && !('statusCode' in body) + + return res.status(status).json(ours ? body : { error: TEXT[status] ?? 'Error' }) + } + // body-parser errors (oversized or malformed JSON) carry their own status + const status = (exception as { status?: number })?.status + + if (typeof status === 'number' && status >= 400 && status < 500) { + return res.status(status).json({ error: TEXT[status] ?? 'Bad Request' }) + } + this.logger.error(exception instanceof Error ? (exception.stack ?? exception.message) : String(exception)) + + return res.status(500).json({ error: 'Internal Server Error' }) + } +} diff --git a/api/src/shared/http/no-store.interceptor.ts b/api/src/shared/http/no-store.interceptor.ts new file mode 100644 index 0000000..65e4d76 --- /dev/null +++ b/api/src/shared/http/no-store.interceptor.ts @@ -0,0 +1,13 @@ +import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common' +import type { Response } from 'express' +import { Observable } from 'rxjs' + +/** Bank data must never be cached by a browser or a proxy. */ +@Injectable() +export class NoStoreInterceptor implements NestInterceptor { + intercept(context: ExecutionContext, next: CallHandler): Observable { + context.switchToHttp().getResponse().setHeader('Cache-Control', 'no-store') + + return next.handle() + } +} diff --git a/api/src/shared/http/request-logger.middleware.ts b/api/src/shared/http/request-logger.middleware.ts new file mode 100644 index 0000000..e6ca090 --- /dev/null +++ b/api/src/shared/http/request-logger.middleware.ts @@ -0,0 +1,19 @@ +import { Injectable, Logger, NestMiddleware } from '@nestjs/common' +import type { NextFunction, Request, Response } from 'express' + +// The bank's return URL carries a one-time code and state. Keep them out of the logs. +export const redact = (line: string) => line.replace(/([?&](?:code|state)=)[^&\s]+/g, '$1[redacted]') + +@Injectable() +export class RequestLoggerMiddleware implements NestMiddleware { + private readonly logger = new Logger('HTTP') + + use(req: Request, res: Response, next: NextFunction) { + const started = Date.now() + + res.on('finish', () => + this.logger.log(redact(`${req.method} ${req.originalUrl} ${res.statusCode} ${Date.now() - started}ms`)), + ) + next() + } +} diff --git a/api/src/shared/http/request-logger.test.ts b/api/src/shared/http/request-logger.test.ts new file mode 100644 index 0000000..eb835f2 --- /dev/null +++ b/api/src/shared/http/request-logger.test.ts @@ -0,0 +1,13 @@ +import { describe, it, expect } from 'vitest' +import { redact } from './request-logger.middleware' + +describe('logging', () => { + it('redacts the one-time code and state', () => { + expect(redact('GET /api/banking/callback?code=abc123&state=xyz789 302 4ms')).toBe( + 'GET /api/banking/callback?code=[redacted]&state=[redacted] 302 4ms', + ) + }) + it('leaves other URLs alone', () => { + expect(redact('GET /api/learn/mappings 200 1ms')).toBe('GET /api/learn/mappings 200 1ms') + }) +}) diff --git a/api/src/shared/http/zod-validation.pipe.ts b/api/src/shared/http/zod-validation.pipe.ts new file mode 100644 index 0000000..d1c0da8 --- /dev/null +++ b/api/src/shared/http/zod-validation.pipe.ts @@ -0,0 +1,32 @@ +import { BadRequestException, PipeTransform } from '@nestjs/common' +import { ZodType } from 'zod' + +interface Options { + /** Error text in the response body */ + message?: string + /** Body shape the existing clients expect: { error, details } or { success: false, error: issues } */ + shape?: 'error-details' | 'success-false' +} + +/** Validates a request body against a zod schema and passes on the parsed value. */ +export class ZodValidationPipe implements PipeTransform { + constructor( + private readonly schema: ZodType, + private readonly options: Options = {}, + ) {} + + transform(value: unknown): T { + const result = this.schema.safeParse(value) + + if (result.success) { + return result.data + } + const { message = 'Invalid request', shape = 'error-details' } = this.options + + throw new BadRequestException( + shape === 'success-false' + ? { success: false, error: result.error.issues } + : { error: message, details: result.error.issues }, + ) + } +} diff --git a/api/src/sheet-export/a1.ts b/api/src/sheet-export/a1.ts new file mode 100644 index 0000000..046d9a9 --- /dev/null +++ b/api/src/sheet-export/a1.ts @@ -0,0 +1,18 @@ +import { z } from 'zod' + +/** A spreadsheet's id as it appears in its URL. */ +export const SheetIdSchema = z.string().regex(/^[A-Za-z0-9_-]{1,100}$/) + +/** Tab names: up to 100 characters, none of the characters Google itself forbids, no control characters. */ +export const TabNameSchema = z + .string() + .min(1) + .max(100) + // oxlint-disable-next-line no-control-regex + .refine(name => !/[\u0000-\u001f\u007f[\]*?:/\\]/.test(name)) + +/** Column letters: A to ZZZ. */ +export const ColumnSchema = z.string().regex(/^[A-Z]{1,3}$/) + +/** A tab name made safe to put in front of "!" in a range: quoted, with quotes inside doubled. */ +export const quoteSheetTabName = (tabName: string) => `'${tabName.replace(/'/g, "''")}'` diff --git a/api/src/services/sheets.ts b/api/src/sheet-export/google-sheets.service.ts similarity index 54% rename from api/src/services/sheets.ts rename to api/src/sheet-export/google-sheets.service.ts index f083c8e..eb5553a 100644 --- a/api/src/services/sheets.ts +++ b/api/src/sheet-export/google-sheets.service.ts @@ -1,144 +1,148 @@ +import { Injectable } from '@nestjs/common' import { google, sheets_v4 } from 'googleapis' +import { quoteSheetTabName } from './a1' interface CategoryValue { - column: string; - value: number; + column: string + value: number } interface ExportData { - sheetId: string; - tabName: string; - month: string; - monthFormat: 'short' | 'long' | 'numeric'; - categoryValues: CategoryValue[]; - income: number; - savings?: number; - investment?: number; - constantColumns?: string[]; - incomeColumn?: string; - savingsColumn?: string; - investmentColumn?: string; + sheetId: string + tabName: string + month: string + monthFormat: 'short' | 'long' | 'numeric' + categoryValues: CategoryValue[] + income: number + savings?: number + investment?: number + constantColumns?: string[] + incomeColumn?: string + savingsColumn?: string + investmentColumn?: string } interface ExportResult { - success: boolean; - updatedCells: number; - rowNumber: number; + success: boolean + updatedCells: number + rowNumber: number } // Fallbacks used only when the caller doesn't say which columns hold these values. // Category columns are never hardcoded: they come from the request (read from the sheet header row). -const DEFAULT_INCOME_COLUMN = 'T'; -const DEFAULT_SAVINGS_COLUMN = 'V'; -const DEFAULT_INVESTMENT_COLUMN = 'W'; +const DEFAULT_INCOME_COLUMN = 'T' +const DEFAULT_SAVINGS_COLUMN = 'V' +const DEFAULT_INVESTMENT_COLUMN = 'W' async function getAuthClient() { - const keyPath = process.env.GOOGLE_SERVICE_ACCOUNT_KEY_PATH; - + const keyPath = process.env.GOOGLE_SERVICE_ACCOUNT_KEY_PATH + if (!keyPath) { - throw new Error('GOOGLE_SERVICE_ACCOUNT_KEY_PATH not configured'); + throw new Error('GOOGLE_SERVICE_ACCOUNT_KEY_PATH not configured') } const auth = new google.auth.GoogleAuth({ keyFile: keyPath, scopes: ['https://www.googleapis.com/auth/spreadsheets'], - }); + }) - return auth.getClient(); + return auth.getClient() } async function findMonthRow( sheets: sheets_v4.Sheets, sheetId: string, tabName: string, - month: string + month: string, ): Promise { // Get column A (month column) data const response = await sheets.spreadsheets.values.get({ spreadsheetId: sheetId, - range: `${tabName}!A:A`, - }); + range: `${quoteSheetTabName(tabName)}!A:A`, + }) + + const values = response.data.values || [] + const monthLower = month.toLowerCase() - const values = response.data.values || []; - const monthLower = month.toLowerCase(); - // Find matching month row // Matches: "July 2025", "Jul 2025", "07/2025", etc. for (let i = 0; i < values.length; i++) { - const cellValue = values[i]?.[0]?.toString().toLowerCase(); - if (!cellValue) continue; - + const cellValue = values[i]?.[0]?.toString().toLowerCase() + + if (!cellValue) { + continue + } + // Check for exact match or partial match if (cellValue === monthLower || cellValue.includes(monthLower)) { - return i + 1; // 1-indexed row number + return i + 1 // 1-indexed row number } - + // Also try matching just the month name - const monthName = month.split(' ')[0]?.toLowerCase(); + const monthName = month.split(' ')[0]?.toLowerCase() + if (monthName && cellValue.includes(monthName)) { // Verify year matches if present - const year = month.split(' ')[1]; + const year = month.split(' ')[1] + if (!year || cellValue.includes(year)) { - return i + 1; + return i + 1 } } } - throw new Error(`Month "${month}" not found in sheet. Please ensure the row exists in column A.`); + throw new Error(`Month "${month}" not found in sheet. Please ensure the row exists in column A.`) } -export async function exportToSheets(data: ExportData): Promise { - const authClient = await getAuthClient(); - const sheets = google.sheets({ version: 'v4', auth: authClient as any }); +async function writeRow(data: ExportData): Promise { + const authClient = await getAuthClient() + const sheets = google.sheets({ version: 'v4', auth: authClient as any }) + + const tab = quoteSheetTabName(data.tabName) // Find the row for the specified month - const rowNumber = await findMonthRow( - sheets, - data.sheetId, - data.tabName, - data.month - ); + const rowNumber = await findMonthRow(sheets, data.sheetId, data.tabName, data.month) // Prepare batch update data - const updateData: sheets_v4.Schema$ValueRange[] = []; - const constantCols = new Set(data.constantColumns || ['S', 'U', 'X']); // Default: skip formula columns + const updateData: sheets_v4.Schema$ValueRange[] = [] + const constantCols = new Set(data.constantColumns || ['S', 'U', 'X']) // Default: skip formula columns // Category values (columns come from the sheet's header row) for (const { column, value } of data.categoryValues) { // Skip constant/formula columns if (constantCols.has(column)) { - continue; + continue } updateData.push({ - range: `${data.tabName}!${column}${rowNumber}`, + range: `${tab}!${column}${rowNumber}`, values: [[value]], - }); + }) } - const incomeColumn = data.incomeColumn || DEFAULT_INCOME_COLUMN; - const savingsColumn = data.savingsColumn || DEFAULT_SAVINGS_COLUMN; - const investmentColumn = data.investmentColumn || DEFAULT_INVESTMENT_COLUMN; + const incomeColumn = data.incomeColumn || DEFAULT_INCOME_COLUMN + const savingsColumn = data.savingsColumn || DEFAULT_SAVINGS_COLUMN + const investmentColumn = data.investmentColumn || DEFAULT_INVESTMENT_COLUMN if (!constantCols.has(incomeColumn)) { updateData.push({ - range: `${data.tabName}!${incomeColumn}${rowNumber}`, + range: `${tab}!${incomeColumn}${rowNumber}`, values: [[data.income]], - }); + }) } if (data.savings !== undefined && !constantCols.has(savingsColumn)) { updateData.push({ - range: `${data.tabName}!${savingsColumn}${rowNumber}`, + range: `${tab}!${savingsColumn}${rowNumber}`, values: [[data.savings]], - }); + }) } if (data.investment !== undefined && !constantCols.has(investmentColumn)) { updateData.push({ - range: `${data.tabName}!${investmentColumn}${rowNumber}`, + range: `${tab}!${investmentColumn}${rowNumber}`, values: [[data.investment]], - }); + }) } if (updateData.length === 0) { @@ -146,7 +150,7 @@ export async function exportToSheets(data: ExportData): Promise { success: true, updatedCells: 0, rowNumber, - }; + } } // Execute batch update @@ -156,22 +160,22 @@ export async function exportToSheets(data: ExportData): Promise { valueInputOption: 'USER_ENTERED', data: updateData, }, - }); + }) return { success: true, updatedCells: response.data.totalUpdatedCells || 0, rowNumber, - }; + } } -export async function getSheetMetadata(sheetId: string) { - const authClient = await getAuthClient(); - const sheets = google.sheets({ version: 'v4', auth: authClient as any }); +async function readMetadata(sheetId: string) { + const authClient = await getAuthClient() + const sheets = google.sheets({ version: 'v4', auth: authClient as any }) const response = await sheets.spreadsheets.get({ spreadsheetId: sheetId, - }); + }) return { title: response.data.properties?.title, @@ -179,32 +183,35 @@ export async function getSheetMetadata(sheetId: string) { title: s.properties?.title, index: s.properties?.index, })), - }; + } } function columnLetter(index: number): string { - let n = index + 1; - let out = ''; + let n = index + 1 + let out = '' + while (n > 0) { - const rem = (n - 1) % 26; - out = String.fromCharCode(65 + rem) + out; - n = Math.floor((n - 1) / 26); + const rem = (n - 1) % 26 + + out = String.fromCharCode(65 + rem) + out + n = Math.floor((n - 1) / 26) } - return out; + + return out } function normalizeHeader(value: string): string { - return value.toLowerCase().replace(/[^a-z0-9]/g, ''); + return value.toLowerCase().replace(/[^a-z0-9]/g, '') } -export interface SheetHeaderColumn { - column: string; - name: string; +interface SheetHeaderColumn { + column: string + name: string } -export interface SheetLayoutResult { - headerRow: number; // 1-indexed - columns: SheetHeaderColumn[]; +interface SheetLayoutResult { + headerRow: number // 1-indexed + columns: SheetHeaderColumn[] } /** @@ -212,36 +219,53 @@ export interface SheetLayoutResult { * Sheets often have a banner above the headers ("ACTUAL EXPENSES"), so we look through the * first rows for the one that contains "Total Expenditure", falling back to the fullest row. */ -export async function getSheetLayout(sheetId: string, tabName: string): Promise { - const authClient = await getAuthClient(); - const sheets = google.sheets({ version: 'v4', auth: authClient as any }); +async function readLayout(sheetId: string, tabName: string): Promise { + const authClient = await getAuthClient() + const sheets = google.sheets({ version: 'v4', auth: authClient as any }) - const quotedTab = `'${tabName.replace(/'/g, "''")}'`; const response = await sheets.spreadsheets.values.get({ spreadsheetId: sheetId, - range: `${quotedTab}!A1:ZZ10`, - }); + range: `${quoteSheetTabName(tabName)}!A1:ZZ10`, + }) + + const rows = (response.data.values || []).map(r => r.map((c: unknown) => (c ?? '').toString().trim())) - const rows = (response.data.values || []).map(r => r.map((c: unknown) => (c ?? '').toString().trim())); if (rows.length === 0) { - throw new Error(`Tab "${tabName}" is empty.`); + throw new Error(`Tab "${tabName}" is empty.`) } - let headerIndex = rows.findIndex(r => r.some(c => normalizeHeader(c) === 'totalexpenditure')); + let headerIndex = rows.findIndex(r => r.some(c => normalizeHeader(c) === 'totalexpenditure')) + if (headerIndex === -1) { - let best = -1; + let best = -1 + rows.forEach((r, i) => { - const filled = r.filter(Boolean).length; + const filled = r.filter(Boolean).length + if (filled > best) { - best = filled; - headerIndex = i; + best = filled + headerIndex = i } - }); + }) } - const columns = rows[headerIndex] - .map((name, i) => ({ column: columnLetter(i), name })) - .filter(c => c.name); + const columns = rows[headerIndex].map((name, i) => ({ column: columnLetter(i), name })).filter(c => c.name) - return { headerRow: headerIndex + 1, columns }; + return { headerRow: headerIndex + 1, columns } +} + +/** Talks to Google Sheets with a service account (optional: the export screen can also just copy a row). */ +@Injectable() +export class GoogleSheetsService { + exportRow(data: ExportData): Promise { + return writeRow(data) + } + + metadata(sheetId: string) { + return readMetadata(sheetId) + } + + layout(sheetId: string, tabName: string): Promise { + return readLayout(sheetId, tabName) + } } diff --git a/api/src/sheet-export/sheet-export.controller.ts b/api/src/sheet-export/sheet-export.controller.ts new file mode 100644 index 0000000..cb8ec9e --- /dev/null +++ b/api/src/sheet-export/sheet-export.controller.ts @@ -0,0 +1,89 @@ +import { + HttpCode, + BadRequestException, + Body, + Controller, + InternalServerErrorException, + Logger, + Post, + UseGuards, +} from '@nestjs/common' +import { AccessGuard } from '../access/access.guard' +import { CsrfGuard } from '../access/csrf.guard' +import { ZodValidationPipe } from '../shared/http/zod-validation.pipe' +import { GoogleSheetsService } from './google-sheets.service' +import { ExportRequest, ExportRequestSchema, LayoutRequestSchema, MetadataRequestSchema } from './sheet-export.schemas' + +@Controller('api/export') +@UseGuards(AccessGuard, CsrfGuard) +export class SheetExportController { + private readonly logger = new Logger(SheetExportController.name) + + constructor(private readonly sheets: GoogleSheetsService) {} + + // Write a month's figures into the sheet + @Post('sheets') + @HttpCode(200) // the previous API answered 200, not 201 + async exportRow( + @Body( + new ZodValidationPipe(ExportRequestSchema, { + message: 'Invalid request body', + }), + ) + body: ExportRequest, + ) { + return { + success: true, + data: await this.run(() => this.sheets.exportRow(body), 'Failed to export to Google Sheets'), + } + } + + // Read the sheet's header row so the app mirrors the real column layout + @Post('sheets/layout') + @HttpCode(200) // the previous API answered 200, not 201 + async layout(@Body() body: { sheetId?: string; tabName?: string }) { + if (!body?.sheetId || !body?.tabName) { + throw new BadRequestException({ error: 'Missing sheetId or tabName' }) + } + + const ok = LayoutRequestSchema.safeParse(body) + + if (!ok.success) { + throw new BadRequestException({ error: 'Invalid sheetId or tabName' }) + } + + return { + success: true, + data: await this.run(() => this.sheets.layout(ok.data.sheetId, ok.data.tabName), 'Failed to read sheet layout'), + } + } + + // The tabs available in the sheet + @Post('sheets/metadata') + @HttpCode(200) // the previous API answered 200, not 201 + async metadata(@Body() body: { sheetId?: string }) { + if (!body?.sheetId) { + throw new BadRequestException({ error: 'Missing sheetId' }) + } + + const ok = MetadataRequestSchema.safeParse(body) + + if (!ok.success) { + throw new BadRequestException({ error: 'Invalid sheetId' }) + } + + return { + success: true, + data: await this.run(() => this.sheets.metadata(ok.data.sheetId), 'Failed to fetch sheet metadata'), + } + } + + private async run(action: () => Promise, fallback: string): Promise { + try { + return await action() + } catch (error) { + this.logger.error(error instanceof Error ? (error.stack ?? error.message) : String(error)) + throw new InternalServerErrorException({ error: fallback }) + } + } +} diff --git a/api/src/sheet-export/sheet-export.module.ts b/api/src/sheet-export/sheet-export.module.ts new file mode 100644 index 0000000..b5e713d --- /dev/null +++ b/api/src/sheet-export/sheet-export.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common' +import { GoogleSheetsService } from './google-sheets.service' +import { SheetExportController } from './sheet-export.controller' + +@Module({ + controllers: [SheetExportController], + providers: [GoogleSheetsService], +}) +export class SheetExportModule {} diff --git a/api/src/sheet-export/sheet-export.schemas.ts b/api/src/sheet-export/sheet-export.schemas.ts new file mode 100644 index 0000000..d33bf0e --- /dev/null +++ b/api/src/sheet-export/sheet-export.schemas.ts @@ -0,0 +1,21 @@ +import { z } from 'zod' +import { ColumnSchema, SheetIdSchema, TabNameSchema } from './a1' + +export const ExportRequestSchema = z.object({ + sheetId: SheetIdSchema, + tabName: TabNameSchema, + month: z.string(), + monthFormat: z.enum(['short', 'long', 'numeric']), + categoryValues: z.array(z.object({ column: ColumnSchema, value: z.number() })), + income: z.number(), + savings: z.number().optional(), + investment: z.number().optional(), + constantColumns: z.array(ColumnSchema).optional(), + incomeColumn: ColumnSchema.optional(), + savingsColumn: ColumnSchema.optional(), + investmentColumn: ColumnSchema.optional(), +}) +export type ExportRequest = z.infer + +export const LayoutRequestSchema = z.object({ sheetId: SheetIdSchema, tabName: TabNameSchema }) +export const MetadataRequestSchema = z.object({ sheetId: SheetIdSchema }) diff --git a/api/src/testing/test-app.ts b/api/src/testing/test-app.ts new file mode 100644 index 0000000..168eb08 --- /dev/null +++ b/api/src/testing/test-app.ts @@ -0,0 +1,82 @@ +import { INestApplication } from '@nestjs/common' +import { Test } from '@nestjs/testing' +import { AccessModule } from '../access/access.module' +import { ACCESS_VERIFIER, AccessVerifier } from '../access/access.settings' +import { BankConnectionsModule } from '../bank-connections/bank-connections.module' +import { BANK_PROVIDER, BankProvider } from '../bank-connections/providers/bank-provider' +import { CLOCK, Clock } from '../bank-connections/config/bank-connections.tokens' +import { configureApp } from '../app.setup' +import { HealthModule } from '../health/health.module' +import { MerchantRulesModule } from '../merchant-rules/merchant-rules.module' +import { SettingsModule } from '../settings/settings.module' +import { SheetExportModule } from '../sheet-export/sheet-export.module' +import { DatabaseModule } from '../shared/database/database.module' +import { UsersModule } from '../users/users.module' +import { DatabaseService } from '../shared/database/database.service' +import { BankConnectionsStore } from '../bank-connections/connections/bank-connections.store' + +export const TEST_KEY = Buffer.alloc(32, 9) +export const SIGNED_IN_HEADERS = { + 'Content-Type': 'application/json', + 'X-FinKit': '1', +} + +export interface TestAppOptions { + env?: NodeJS.ProcessEnv + provider?: BankProvider + verify?: AccessVerifier | null + clock?: Clock + /** Use a real database file instead of memory (to test upgrades of existing data). */ + databaseFile?: string +} + +/** The whole app, wired the same way as in production, on an in-memory database. */ +export async function createTestApp(options: TestAppOptions = {}) { + const env = { + NODE_ENV: 'test', + BANKING_PROVIDER: 'mock', + BANKING_ENCRYPTION_KEY: TEST_KEY.toString('base64'), + ...options.env, + } + let builder = Test.createTestingModule({ + imports: [ + DatabaseModule.forRoot(options.databaseFile ?? ':memory:'), + UsersModule, + AccessModule.forRoot(env), + HealthModule, + MerchantRulesModule, + SettingsModule, + SheetExportModule, + BankConnectionsModule.forRoot(env), + ], + }) + + if (options.provider) { + builder = builder.overrideProvider(BANK_PROVIDER).useValue(options.provider) + } + if (options.clock) { + builder = builder.overrideProvider(CLOCK).useValue(options.clock) + } + if (options.verify !== undefined) { + builder = builder.overrideProvider(ACCESS_VERIFIER).useValue(options.verify) + } + const moduleRef = await builder.compile() + const app: INestApplication = moduleRef.createNestApplication() + + configureApp(app, env) + await app.init() + + return { + app, + server: app.getHttpServer(), + db: moduleRef.get(DatabaseService).db, + store: (() => { + try { + return moduleRef.get(BankConnectionsStore) + } catch { + return undefined + } + })(), + close: () => app.close(), + } +} diff --git a/api/src/users/users.module.ts b/api/src/users/users.module.ts new file mode 100644 index 0000000..c172d31 --- /dev/null +++ b/api/src/users/users.module.ts @@ -0,0 +1,7 @@ +import { Global, Module } from '@nestjs/common' +import { UsersRepository } from './users.repository' +import { UsersService } from './users.service' + +@Global() +@Module({ providers: [UsersRepository, UsersService], exports: [UsersService] }) +export class UsersModule {} diff --git a/api/src/users/users.repository.ts b/api/src/users/users.repository.ts new file mode 100644 index 0000000..ff1538a --- /dev/null +++ b/api/src/users/users.repository.ts @@ -0,0 +1,56 @@ +import { Injectable } from '@nestjs/common' +import { randomUUID } from 'node:crypto' +import { DatabaseService } from '../shared/database/database.service' +import { hasColumn } from '../shared/database/migrate' + +export interface UserRow { + id: string + email: string + created_at: string + last_seen_at: string +} + +/** The people who use this FinKit. Identity comes from the sign-in (Cloudflare Access), never from a password here. */ +@Injectable() +export class UsersRepository { + private readonly byEmail + private readonly insert + private readonly touchSeen + + constructor(database: DatabaseService) { + const { db } = database + + db.exec(` + CREATE TABLE IF NOT EXISTS users ( + id TEXT PRIMARY KEY, + email TEXT NOT NULL UNIQUE, + created_at TEXT NOT NULL, + last_seen_at TEXT NOT NULL + ); + `) + // An earlier version marked an "owner" here. Everyone is equal now. + if (hasColumn(db, 'users', 'is_owner')) { + db.exec('ALTER TABLE users DROP COLUMN is_owner') + } + this.byEmail = db.prepare('SELECT * FROM users WHERE email = ?') + this.insert = db.prepare('INSERT INTO users (id, email, created_at, last_seen_at) VALUES (?, ?, ?, ?)') + this.touchSeen = db.prepare('UPDATE users SET last_seen_at = ? WHERE id = ?') + } + + findByEmail(email: string) { + return this.byEmail.get(email) as UserRow | undefined + } + + create(email: string): UserRow { + const now = new Date().toISOString() + const row = { id: randomUUID(), email, created_at: now, last_seen_at: now } + + this.insert.run(row.id, row.email, row.created_at, row.last_seen_at) + + return row + } + + touch(id: string) { + this.touchSeen.run(new Date().toISOString(), id) + } +} diff --git a/api/src/users/users.service.ts b/api/src/users/users.service.ts new file mode 100644 index 0000000..a7f3b9b --- /dev/null +++ b/api/src/users/users.service.ts @@ -0,0 +1,62 @@ +import { Inject, Injectable, Logger, OnModuleInit } from '@nestjs/common' +import { ACCESS_SETTINGS, AccessSettings } from '../access/access.settings' +import { UsersRepository } from './users.repository' + +export interface User { + id: string + email: string +} + +/** Hands data that predates user accounts to the person who should inherit it. */ +type LegacyClaimer = (userId: string) => void + +@Injectable() +export class UsersService implements OnModuleInit { + private readonly claimers: LegacyClaimer[] = [] + private readonly logger = new Logger(UsersService.name) + + constructor( + private readonly users: UsersRepository, + @Inject(ACCESS_SETTINGS) private readonly settings: AccessSettings, + ) {} + + onModuleInit() { + if (this.settings.allowedEmails.length === 0) { + this.logger.error('No one is allowed in: set ALLOWED_USER_EMAILS. Every request will be refused.') + } + } + + /** Stores register how to adopt their old, ownerless rows. They run once, when that person's profile is created. */ + onLegacyOwnerCreated(claim: LegacyClaimer) { + this.claimers.push(claim) + } + + /** + * The user for a signed-in email, or null when that email is not on the allowed list. + * The list is checked on every request, so taking someone off it locks them out at once. + * Everyone on it is equal. Data from before accounts goes to the first address listed. + */ + ensure(rawEmail: string): User | null { + const email = rawEmail.trim().toLowerCase() + + if (!this.settings.allowedEmails.includes(email)) { + return null + } + const existing = this.users.findByEmail(email) + + if (existing) { + this.users.touch(existing.id) + + return { id: existing.id, email } + } + const created = this.users.create(email) + + if (email === this.settings.legacyOwner) { + for (const claim of this.claimers) { + claim(created.id) + } + } + + return { id: created.id, email } + } +} diff --git a/api/src/users/users.test.ts b/api/src/users/users.test.ts new file mode 100644 index 0000000..2307ddf --- /dev/null +++ b/api/src/users/users.test.ts @@ -0,0 +1,272 @@ +import { afterEach, describe, expect, it } from 'vitest' +import request from 'supertest' +import Database from 'better-sqlite3' +import { mkdtempSync, rmSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { SIGNED_IN_HEADERS as H, TestAppOptions, createTestApp } from '../testing/test-app' + +type TestApp = Awaited> +const open: TestApp[] = [] +const dirs: string[] = [] + +async function make(options?: TestAppOptions) { + const t = await createTestApp(options) + + open.push(t) + + return t +} +afterEach(async () => { + while (open.length) { + await open.pop()!.close() + } + while (dirs.length) { + rmSync(dirs.pop()!, { recursive: true, force: true }) + } +}) + +const as = (who: string) => ({ ...H, 'X-Dev-User': who }) +const ALICE = 'alice@x.com' +const BOB = 'bob@x.com' +const household = { ALLOWED_USER_EMAILS: `${ALICE},${BOB}` } + +describe('users: whose data is whose', () => { + it('keeps each person’s learned rules apart', async () => { + const t = await make({ env: household }) + + await request(t.server) + .post('/api/learn/mapping') + .set(as(ALICE)) + .send({ merchant: 'DeepSeek', category: 'Subscriptions' }) + .expect(200) + await request(t.server) + .post('/api/learn/mapping') + .set(as(BOB)) + .send({ merchant: 'DeepSeek', category: 'Software' }) + .expect(200) + + const alice = (await request(t.server).get('/api/learn/mappings').set(as(ALICE))).body.data + const bob = (await request(t.server).get('/api/learn/mappings').set(as(BOB))).body.data + + expect(alice.map((r: { category: string }) => r.category)).toEqual(['Subscriptions']) + expect(bob.map((r: { category: string }) => r.category)).toEqual(['Software']) + + await request(t.server).delete('/api/learn/mapping/DeepSeek').set(as(BOB)).expect(200) + expect((await request(t.server).get('/api/learn/mappings').set(as(ALICE))).body.data).toHaveLength(1) + }) + + it('keeps each person’s settings apart', async () => { + const t = await make({ env: household }) + + await request(t.server).post('/api/settings/theme').set(as(ALICE)).send({ value: 'dark' }).expect(200) + await request(t.server).get('/api/settings/theme').set(as(BOB)).expect(404) + expect((await request(t.server).get('/api/settings/theme').set(as(ALICE)).expect(200)).body.data).toBe('dark') + }) + + it('keeps bank connections private to whoever made them', async () => { + const t = await make({ env: household }) + const start = await request(t.server).post('/api/banking/connect').set(as(ALICE)).send({ bank: 'N26' }).expect(200) + const state = new URL(start.body.url, 'http://x').searchParams.get('state')! + const decide = await request(t.server) + .get(`/api/banking/mock-bank/decide?state=${encodeURIComponent(state)}&d=approve`) + .expect(302) + const cookie = String(start.headers['set-cookie']?.[0] ?? '').split(';')[0] + + // The bank's return carries no sign-in: the connection belongs to whoever started it + await request(t.server).get(String(decide.headers.location)).set('Cookie', cookie).expect(302) + + const mine = (await request(t.server).get('/api/banking/connections').set(as(ALICE))).body.connections + + expect(mine).toHaveLength(1) + const id = mine[0].id + + expect((await request(t.server).get('/api/banking/connections').set(as(BOB))).body.connections).toEqual([]) + await request(t.server).post(`/api/banking/connections/${id}/sync`).set(as(BOB)).send({}).expect(404) + await request(t.server).delete(`/api/banking/connections/${id}`).set(as(BOB)).expect(404) + await request(t.server).post(`/api/banking/connections/${id}/sync`).set(as(ALICE)).send({}).expect(200) + }) +}) + +describe('users: the back office is private too', () => { + it('shows each person only their own sync history, audit trail and counts', async () => { + const t = await make({ env: household }) + const start = await request(t.server).post('/api/banking/connect').set(as(ALICE)).send({ bank: 'N26' }).expect(200) + const state = new URL(start.body.url, 'http://x').searchParams.get('state')! + const decide = await request(t.server) + .get(`/api/banking/mock-bank/decide?state=${encodeURIComponent(state)}&d=approve`) + .expect(302) + const cookie = String(start.headers['set-cookie']?.[0] ?? '').split(';')[0] + + await request(t.server).get(String(decide.headers.location)).set('Cookie', cookie).expect(302) + const id = (await request(t.server).get('/api/banking/connections').set(as(ALICE))).body.connections[0].id + + await request(t.server).post(`/api/banking/connections/${id}/sync`).set(as(ALICE)).send({}).expect(200) + + const trail = (who: string) => request(t.server).get('/api/banking/admin/audit').set(as(who)) + const alice = (await trail(ALICE)).body.entries.map((e: { action: string }) => e.action) + + expect(alice).toEqual(expect.arrayContaining(['connect.start', 'connect.complete', 'sync'])) + expect((await trail(BOB)).body.entries).toEqual([]) + + expect((await request(t.server).get('/api/banking/admin/sync-log').set(as(ALICE))).body.entries).toHaveLength(1) + expect((await request(t.server).get('/api/banking/admin/sync-log').set(as(BOB))).body.entries).toEqual([]) + + const counts = async (who: string) => (await request(t.server).get('/api/banking/admin/overview').set(as(who))).body + + expect((await counts(ALICE)).connections.total).toBe(1) + expect((await counts(BOB)).connections.total).toBe(0) + expect((await counts(BOB)).syncs30d.total).toBe(0) + }) +}) + +describe('users: who may sign in', () => { + it('lets listed people in and refuses strangers', async () => { + const t = await make({ env: { ALLOWED_USER_EMAILS: ALICE } }) + + await request(t.server).get('/api/learn/mappings').expect(200) // locally the developer is always in + await request(t.server).get('/api/learn/mappings').set(as('mallory@x.com')).expect(403) + await request(t.server).get('/api/learn/mappings').set(as(ALICE)).expect(200) + }) + + it('refuses everyone when no one is listed outside local development', async () => { + const t = await make({ + env: { CF_ACCESS_TEAM_DOMAIN: 't', CF_ACCESS_AUD: 'a' }, + verify: async () => ({ email: 'me@x.com' }), + }) + + expect( + ( + await request(t.server) + .get('/api/learn/mappings') + .set({ ...H, 'cf-access-jwt-assertion': 't' }) + ).status, + ).toBe(403) + }) + + it('locks someone out as soon as they are taken off the list', async () => { + const dir = mkdtempSync(join(tmpdir(), 'finkit-')) + + dirs.push(dir) + const file = join(dir, 'people.db') + const cf = { CF_ACCESS_TEAM_DOMAIN: 't', CF_ACCESS_AUD: 'a' } + const verify = async () => ({ email: BOB }) + const headers = { ...H, 'cf-access-jwt-assertion': 't' } + const first = await make({ + env: { ...cf, ALLOWED_USER_EMAILS: `${ALICE},${BOB}` }, + verify, + databaseFile: file, + }) + + await request(first.server).get('/api/learn/mappings').set(headers).expect(200) + await first.close() + open.pop() + // Same database, Bob no longer listed: he already has an account but must not get in + const second = await make({ + env: { ...cf, ALLOWED_USER_EMAILS: ALICE }, + verify, + databaseFile: file, + }) + + await request(second.server).get('/api/learn/mappings').set(headers).expect(403) + }) + + it('does not trust the X-Dev-User header in production', async () => { + const t = await make({ + env: { + NODE_ENV: 'production', + ALLOWED_ORIGIN: 'https://finkit.test', + BANKING_PROVIDER: 'off', + CF_ACCESS_TEAM_DOMAIN: 't', + CF_ACCESS_AUD: 'a', + ALLOWED_USER_EMAILS: 'me@x.com', + }, + verify: async () => ({ email: 'me@x.com' }), + }) + const tok = { 'Cf-Access-Jwt-Assertion': 'tok' } + + await request(t.server) + .post('/api/learn/mapping') + .set(H) + .set(tok) + .set('Origin', 'https://finkit.test') + .send({ merchant: 'Lidl', category: 'Groceries' }) + .expect(200) + // A different name in the header must not switch users: same data comes back + const seen = await request(t.server).get('/api/learn/mappings').set(as('someone@else.com')).set(tok).expect(200) + + expect(seen.body.data).toHaveLength(1) + }) + + it('refuses the API without a token once sign-in is configured', async () => { + const t = await make({ + env: { CF_ACCESS_TEAM_DOMAIN: 't', CF_ACCESS_AUD: 'a' }, + verify: async () => ({ email: 'me@x.com' }), + }) + + for (const [method, path] of [ + ['get', '/api/learn/mappings'], + ['get', '/api/settings/sheet'], + ['post', '/api/export/sheets'], + ] as const) { + expect((await request(t.server)[method](path).set(H)).status, `${method} ${path}`).toBe(401) + } + }) +}) + +describe('users: data from before accounts existed', () => { + it('goes to the first listed address, even when someone else arrives first', async () => { + const dir = mkdtempSync(join(tmpdir(), 'finkit-')) + + dirs.push(dir) + const file = join(dir, 'old.db') + const old = new Database(file) + + old.exec(` + CREATE TABLE merchant_categories (id INTEGER PRIMARY KEY AUTOINCREMENT, merchant_key TEXT NOT NULL UNIQUE, + merchant_display TEXT NOT NULL, category TEXT NOT NULL, confidence INTEGER DEFAULT 1, + created_at TEXT DEFAULT CURRENT_TIMESTAMP, updated_at TEXT DEFAULT CURRENT_TIMESTAMP); + INSERT INTO merchant_categories (merchant_key, merchant_display, category) VALUES ('rewe', 'REWE', 'Groceries'); + `) + old.close() + const t = await make({ env: household, databaseFile: file }) + + // Alice is allowed in and arrives first, but she is not the first address listed, so she inherits nothing. + expect((await request(t.server).get('/api/learn/mappings').set(as(ALICE))).body.data).toEqual([]) + // The developer (listed first) arrives later and still gets everything that existed before accounts + expect((await request(t.server).get('/api/learn/mappings').set(H)).body.data).toHaveLength(1) + expect((await request(t.server).get('/api/learn/mappings').set(as(ALICE))).body.data).toEqual([]) + }) + + it('goes to the first listed address on upgrade, and to nobody else', async () => { + const dir = mkdtempSync(join(tmpdir(), 'finkit-')) + + dirs.push(dir) + const file = join(dir, 'old.db') + // The tables exactly as the previous version created them + const old = new Database(file) + + old.exec(` + CREATE TABLE merchant_categories (id INTEGER PRIMARY KEY AUTOINCREMENT, merchant_key TEXT NOT NULL UNIQUE, + merchant_display TEXT NOT NULL, category TEXT NOT NULL, confidence INTEGER DEFAULT 1, + created_at TEXT DEFAULT CURRENT_TIMESTAMP, updated_at TEXT DEFAULT CURRENT_TIMESTAMP); + CREATE TABLE settings (key TEXT PRIMARY KEY, value TEXT NOT NULL, updated_at TEXT DEFAULT CURRENT_TIMESTAMP); + INSERT INTO merchant_categories (merchant_key, merchant_display, category) VALUES ('rewe', 'REWE', 'Groceries'); + INSERT INTO settings (key, value) VALUES ('theme', '"dark"'); + `) + old.close() + + const t = await make({ env: household, databaseFile: file }) + + // The developer (listed first) inherits it + expect( + (await request(t.server).get('/api/learn/mappings').set(H)).body.data.map( + (r: { merchant: string }) => r.merchant, + ), + ).toEqual(['rewe']) + expect((await request(t.server).get('/api/settings/theme').set(H).expect(200)).body.data).toBe('dark') + // A later arrival sees none of it + expect((await request(t.server).get('/api/learn/mappings').set(as(ALICE))).body.data).toEqual([]) + await request(t.server).get('/api/settings/theme').set(as(ALICE)).expect(404) + }) +}) diff --git a/api/tsconfig.build.json b/api/tsconfig.build.json new file mode 100644 index 0000000..4edcedf --- /dev/null +++ b/api/tsconfig.build.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "exclude": ["node_modules", "dist", "src/**/*.test.ts", "src/**/*.spec.ts", "src/testing/**"] +} diff --git a/api/tsconfig.json b/api/tsconfig.json index 4cbbd9a..fcd72b2 100644 --- a/api/tsconfig.json +++ b/api/tsconfig.json @@ -1,16 +1,21 @@ { "compilerOptions": { - "target": "ES2022", - "module": "ESNext", - "moduleResolution": "bundler", - "lib": ["ES2022"], + "module": "commonjs", + "moduleResolution": "node", + "ignoreDeprecations": "6.0", + "target": "ES2023", + "lib": ["ES2023"], "outDir": "./dist", "rootDir": "./src", "strict": true, + "strictPropertyInitialization": false, "esModuleInterop": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, - "resolveJsonModule": true + "resolveJsonModule": true, + "sourceMap": true }, "include": ["src/**/*"], "exclude": ["node_modules", "dist"] diff --git a/api/vitest.config.mts b/api/vitest.config.mts new file mode 100644 index 0000000..2f23259 --- /dev/null +++ b/api/vitest.config.mts @@ -0,0 +1,9 @@ +import { defineConfig } from 'vitest/config' +import swc from 'unplugin-swc' + +// Nest's dependency injection reads constructor types from decorator metadata, which +// esbuild (Vitest's default) doesn't emit. SWC does. +export default defineConfig({ + test: { include: ['src/**/*.test.ts'] }, + plugins: [swc.vite({ module: { type: 'es6' } })], +}) diff --git a/deploy.sh b/deploy.sh index f862c14..7409bbb 100755 --- a/deploy.sh +++ b/deploy.sh @@ -41,8 +41,8 @@ case "${1:-update}" in update|"") # Check .env if [ ! -f .env ]; then - if [ -f example.env ]; then - cp example.env .env + if [ -f .env.example ]; then + cp .env.example .env warn "Created .env from example. Edit it with your credentials, then run again." exit 1 fi @@ -51,11 +51,9 @@ case "${1:-update}" in # Load env set -a; source .env 2>/dev/null || true; set +a - # GHCR login if credentials provided - if [ -n "$GITHUB_TOKEN" ] && [ -n "$GITHUB_USER" ]; then - info "🔐 Logging in to ghcr.io..." - echo "$GITHUB_TOKEN" | docker login ghcr.io -u "$GITHUB_USER" --password-stdin - fi + # Key files for the API live here (mounted read-only at /run/secrets). Create it ourselves so + # Docker doesn't create it owned by root. + mkdir -p secrets info "📦 Pulling latest images..." docker compose -f $COMPOSE_FILE pull diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 3fd9f75..5cbf052 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -31,6 +31,7 @@ services: - DATA_DIR=/data volumes: - api-data:/data # Persistent storage for SQLite database + - ./secrets:/run/secrets:ro # key files named in .env (EB_PRIVATE_KEY_PATH, GOOGLE_SERVICE_ACCOUNT_KEY_PATH) networks: - finkit-network healthcheck: diff --git a/docker-compose.yml b/docker-compose.yml index 5895383..6cacc3b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -34,7 +34,6 @@ services: - NODE_ENV=production - PORT=3000 - DATA_DIR=/app/data - - OPENAI_API_KEY=${OPENAI_API_KEY:-} - GOOGLE_SERVICE_ACCOUNT_EMAIL=${GOOGLE_SERVICE_ACCOUNT_EMAIL:-} - GOOGLE_PRIVATE_KEY=${GOOGLE_PRIVATE_KEY:-} volumes: diff --git a/docs/enable-banking-mock-transactions.json b/docs/enable-banking-mock-transactions.json new file mode 100644 index 0000000..ea451f8 --- /dev/null +++ b/docs/enable-banking-mock-transactions.json @@ -0,0 +1,515 @@ +[ + { + "entry_reference": "MOCK0027", + "status": "BOOK", + "transaction_amount": { + "amount": "2800.00", + "currency": "EUR" + }, + "credit_debit_indicator": "CRDT", + "booking_date": "2026-11-01", + "value_date": "2026-11-01", + "transaction_date": "2026-11-01", + "remittance_information": [ + "Salary 2026-11" + ], + "debtor": { + "name": "Acme GmbH" + }, + "debtor_account": { + "iban": "DE02120300000000202051" + } + }, + { + "entry_reference": "MOCK0020", + "status": "BOOK", + "transaction_amount": { + "amount": "27.89", + "currency": "EUR" + }, + "credit_debit_indicator": "DBIT", + "booking_date": "2026-10-27", + "value_date": "2026-10-27", + "transaction_date": "2026-10-27", + "remittance_information": [ + "Card payment groceries" + ], + "creditor": { + "name": "REWE Markt GmbH" + } + }, + { + "entry_reference": "MOCK0019", + "status": "BOOK", + "transaction_amount": { + "amount": "21.28", + "currency": "EUR" + }, + "credit_debit_indicator": "DBIT", + "booking_date": "2026-10-24", + "value_date": "2026-10-24", + "transaction_date": "2026-10-24", + "remittance_information": [ + "Card payment groceries" + ], + "creditor": { + "name": "REWE Markt GmbH" + } + }, + { + "entry_reference": "MOCK0018", + "status": "BOOK", + "transaction_amount": { + "amount": "50.40", + "currency": "EUR" + }, + "credit_debit_indicator": "DBIT", + "booking_date": "2026-10-18", + "value_date": "2026-10-18", + "transaction_date": "2026-10-18", + "remittance_information": [ + "Card payment groceries" + ], + "creditor": { + "name": "EDEKA" + } + }, + { + "entry_reference": "MOCK0022", + "status": "BOOK", + "transaction_amount": { + "amount": "19.93", + "currency": "EUR" + }, + "credit_debit_indicator": "DBIT", + "booking_date": "2026-10-18", + "value_date": "2026-10-18", + "transaction_date": "2026-10-18", + "remittance_information": [ + "DeepSeek API credits" + ], + "creditor": { + "name": "DeepSeek" + } + }, + { + "entry_reference": "MOCK0017", + "status": "BOOK", + "transaction_amount": { + "amount": "50.01", + "currency": "EUR" + }, + "credit_debit_indicator": "DBIT", + "booking_date": "2026-10-16", + "value_date": "2026-10-16", + "transaction_date": "2026-10-16", + "remittance_information": [ + "Card payment groceries" + ], + "creditor": { + "name": "REWE Markt GmbH" + } + }, + { + "entry_reference": "MOCK0024", + "status": "BOOK", + "transaction_amount": { + "amount": "500.00", + "currency": "EUR" + }, + "credit_debit_indicator": "DBIT", + "booking_date": "2026-10-15", + "value_date": "2026-10-15", + "transaction_date": "2026-10-15", + "remittance_information": [ + "Transfer to savings" + ], + "creditor": { + "name": "Own savings account" + }, + "creditor_account": { + "iban": "DE89370400440532013000" + } + }, + { + "entry_reference": "MOCK0025", + "status": "BOOK", + "transaction_amount": { + "amount": "500.00", + "currency": "EUR" + }, + "credit_debit_indicator": "CRDT", + "booking_date": "2026-10-15", + "value_date": "2026-10-15", + "transaction_date": "2026-10-15", + "remittance_information": [ + "Transfer from current account" + ], + "debtor": { + "name": "Samson Negedu" + }, + "debtor_account": { + "iban": "DE89370400440532013000" + } + }, + { + "entry_reference": "MOCK0023", + "status": "BOOK", + "transaction_amount": { + "amount": "88.00", + "currency": "EUR" + }, + "credit_debit_indicator": "DBIT", + "booking_date": "2026-10-12", + "value_date": "2026-10-12", + "transaction_date": "2026-10-12", + "remittance_information": [ + "Invoice 20931" + ], + "creditor": { + "name": "Unknown Vendor AG" + } + }, + { + "entry_reference": "MOCK0021", + "status": "BOOK", + "transaction_amount": { + "amount": "56.67", + "currency": "EUR" + }, + "credit_debit_indicator": "DBIT", + "booking_date": "2026-10-06", + "value_date": "2026-10-06", + "transaction_date": "2026-10-06", + "remittance_information": [ + "Fuel" + ], + "creditor": { + "name": "Shell" + } + }, + { + "entry_reference": "MOCK0026", + "status": "BOOK", + "transaction_amount": { + "amount": "750.00", + "currency": "EUR" + }, + "credit_debit_indicator": "DBIT", + "booking_date": "2026-10-05", + "value_date": "2026-10-05", + "transaction_date": "2026-10-05", + "remittance_information": [ + "Rent" + ], + "creditor": { + "name": "Hausverwaltung Mueller" + }, + "creditor_account": { + "iban": "DE44500105175407324931" + } + }, + { + "entry_reference": "MOCK0016", + "status": "BOOK", + "transaction_amount": { + "amount": "10.99", + "currency": "EUR" + }, + "credit_debit_indicator": "DBIT", + "booking_date": "2026-10-03", + "value_date": "2026-10-03", + "transaction_date": "2026-10-03", + "remittance_information": [ + "Spotify Premium" + ], + "creditor": { + "name": "Spotify" + } + }, + { + "entry_reference": "MOCK0015", + "status": "BOOK", + "transaction_amount": { + "amount": "9.99", + "currency": "EUR" + }, + "credit_debit_indicator": "DBIT", + "booking_date": "2026-10-02", + "value_date": "2026-10-02", + "transaction_date": "2026-10-02", + "remittance_information": [ + "Netflix subscription" + ], + "creditor": { + "name": "Netflix" + } + }, + { + "entry_reference": "MOCK0014", + "status": "BOOK", + "transaction_amount": { + "amount": "2800.00", + "currency": "EUR" + }, + "credit_debit_indicator": "CRDT", + "booking_date": "2026-10-01", + "value_date": "2026-10-01", + "transaction_date": "2026-10-01", + "remittance_information": [ + "Salary 2026-10" + ], + "debtor": { + "name": "Acme GmbH" + }, + "debtor_account": { + "iban": "DE02120300000000202051" + } + }, + { + "entry_reference": "MOCK0007", + "status": "BOOK", + "transaction_amount": { + "amount": "41.22", + "currency": "EUR" + }, + "credit_debit_indicator": "DBIT", + "booking_date": "2026-09-22", + "value_date": "2026-09-22", + "transaction_date": "2026-09-22", + "remittance_information": [ + "Card payment groceries" + ], + "creditor": { + "name": "REWE Markt GmbH" + } + }, + { + "entry_reference": "MOCK0005", + "status": "BOOK", + "transaction_amount": { + "amount": "42.74", + "currency": "EUR" + }, + "credit_debit_indicator": "DBIT", + "booking_date": "2026-09-21", + "value_date": "2026-09-21", + "transaction_date": "2026-09-21", + "remittance_information": [ + "Card payment groceries" + ], + "creditor": { + "name": "EDEKA" + } + }, + { + "entry_reference": "MOCK0006", + "status": "BOOK", + "transaction_amount": { + "amount": "59.23", + "currency": "EUR" + }, + "credit_debit_indicator": "DBIT", + "booking_date": "2026-09-20", + "value_date": "2026-09-20", + "transaction_date": "2026-09-20", + "remittance_information": [ + "Card payment groceries" + ], + "creditor": { + "name": "REWE Markt GmbH" + } + }, + { + "entry_reference": "MOCK0004", + "status": "BOOK", + "transaction_amount": { + "amount": "27.87", + "currency": "EUR" + }, + "credit_debit_indicator": "DBIT", + "booking_date": "2026-09-18", + "value_date": "2026-09-18", + "transaction_date": "2026-09-18", + "remittance_information": [ + "Card payment groceries" + ], + "creditor": { + "name": "EDEKA" + } + }, + { + "entry_reference": "MOCK0011", + "status": "BOOK", + "transaction_amount": { + "amount": "500.00", + "currency": "EUR" + }, + "credit_debit_indicator": "DBIT", + "booking_date": "2026-09-15", + "value_date": "2026-09-15", + "transaction_date": "2026-09-15", + "remittance_information": [ + "Transfer to savings" + ], + "creditor": { + "name": "Own savings account" + }, + "creditor_account": { + "iban": "DE89370400440532013000" + } + }, + { + "entry_reference": "MOCK0012", + "status": "BOOK", + "transaction_amount": { + "amount": "500.00", + "currency": "EUR" + }, + "credit_debit_indicator": "CRDT", + "booking_date": "2026-09-15", + "value_date": "2026-09-15", + "transaction_date": "2026-09-15", + "remittance_information": [ + "Transfer from current account" + ], + "debtor": { + "name": "Samson Negedu" + }, + "debtor_account": { + "iban": "DE89370400440532013000" + } + }, + { + "entry_reference": "MOCK0010", + "status": "BOOK", + "transaction_amount": { + "amount": "88.00", + "currency": "EUR" + }, + "credit_debit_indicator": "DBIT", + "booking_date": "2026-09-12", + "value_date": "2026-09-12", + "transaction_date": "2026-09-12", + "remittance_information": [ + "Invoice 20931" + ], + "creditor": { + "name": "Unknown Vendor AG" + } + }, + { + "entry_reference": "MOCK0009", + "status": "BOOK", + "transaction_amount": { + "amount": "22.72", + "currency": "EUR" + }, + "credit_debit_indicator": "DBIT", + "booking_date": "2026-09-09", + "value_date": "2026-09-09", + "transaction_date": "2026-09-09", + "remittance_information": [ + "DeepSeek API credits" + ], + "creditor": { + "name": "DeepSeek" + } + }, + { + "entry_reference": "MOCK0008", + "status": "BOOK", + "transaction_amount": { + "amount": "50.93", + "currency": "EUR" + }, + "credit_debit_indicator": "DBIT", + "booking_date": "2026-09-08", + "value_date": "2026-09-08", + "transaction_date": "2026-09-08", + "remittance_information": [ + "Fuel" + ], + "creditor": { + "name": "Shell" + } + }, + { + "entry_reference": "MOCK0013", + "status": "BOOK", + "transaction_amount": { + "amount": "750.00", + "currency": "EUR" + }, + "credit_debit_indicator": "DBIT", + "booking_date": "2026-09-05", + "value_date": "2026-09-05", + "transaction_date": "2026-09-05", + "remittance_information": [ + "Rent" + ], + "creditor": { + "name": "Hausverwaltung Mueller" + }, + "creditor_account": { + "iban": "DE44500105175407324931" + } + }, + { + "entry_reference": "MOCK0003", + "status": "BOOK", + "transaction_amount": { + "amount": "10.99", + "currency": "EUR" + }, + "credit_debit_indicator": "DBIT", + "booking_date": "2026-09-03", + "value_date": "2026-09-03", + "transaction_date": "2026-09-03", + "remittance_information": [ + "Spotify Premium" + ], + "creditor": { + "name": "Spotify" + } + }, + { + "entry_reference": "MOCK0002", + "status": "BOOK", + "transaction_amount": { + "amount": "9.99", + "currency": "EUR" + }, + "credit_debit_indicator": "DBIT", + "booking_date": "2026-09-02", + "value_date": "2026-09-02", + "transaction_date": "2026-09-02", + "remittance_information": [ + "Netflix subscription" + ], + "creditor": { + "name": "Netflix" + } + }, + { + "entry_reference": "MOCK0001", + "status": "BOOK", + "transaction_amount": { + "amount": "2800.00", + "currency": "EUR" + }, + "credit_debit_indicator": "CRDT", + "booking_date": "2026-09-01", + "value_date": "2026-09-01", + "transaction_date": "2026-09-01", + "remittance_information": [ + "Salary 2026-09" + ], + "debtor": { + "name": "Acme GmbH" + }, + "debtor_account": { + "iban": "DE02120300000000202051" + } + } +] \ No newline at end of file diff --git a/docs/enable-banking-setup.md b/docs/enable-banking-setup.md index b70b001..abb8aaf 100644 --- a/docs/enable-banking-setup.md +++ b/docs/enable-banking-setup.md @@ -63,12 +63,12 @@ Set in `.env` (values you get from the steps above and from Cloudflare): ``` BANKING_PROVIDER=enablebanking EB_APP_ID=... # shown in the Control Panel after registering -EB_PRIVATE_KEY_PATH=/run/secrets/enablebanking.key +EB_PRIVATE_KEY_PATH=/run/secrets/enablebanking.pem BANKING_REDIRECT_URL=https://YOUR-DOMAIN/api/banking/callback BANKING_ENCRYPTION_KEY=... # openssl rand -base64 32 CF_ACCESS_TEAM_DOMAIN=... # the part before .cloudflareaccess.com CF_ACCESS_AUD=... # Access, Applications, your app, Overview: AUD tag -BANKING_ADMIN_EMAILS=you@example.com +ALLOWED_USER_EMAILS=you@example.com # who may use FinKit; without one nobody can sign in ``` -Mount the private key into the API container read-only (for example `./secrets/enablebanking.key:/run/secrets/enablebanking.key:ro`). Never put it in the image or in git. +Mount the private key into the API container read-only (for example `./secrets/enablebanking.pem:/run/secrets/enablebanking.pem:ro`). Never put it in the image or in git. diff --git a/docs/privacy-facts.md b/docs/privacy-facts.md new file mode 100644 index 0000000..4882e30 --- /dev/null +++ b/docs/privacy-facts.md @@ -0,0 +1,55 @@ +# Data handling facts (input for the privacy policy and terms) + +A factual list of what FinKit does with personal data, taken from the code. It is **not** the policy and not legal advice: have someone qualified turn it into the policy and terms. Re-check it whenever storage changes. + +## What is stored on the server + +| Data | Where | Protection | Kept for | +|------|-------|------------|----------| +| Email address, first and last seen | `users` | Plain | Until the database is wiped (no account deletion yet) | +| Learned merchant rules, sheet and display settings | `merchant_categories`, `settings` | Plain | Until the user deletes the rule or setting | +| Bank connection: bank name, status, consent end date, masked account labels (last 4 of IBAN) | `bank_connections` | Plain | Until disconnected (the secret below is erased on disconnect) | +| Bank session id and the accounts' full IBANs | `bank_connections.secret_blob` | Encrypted (AES-256-GCM, key from `BANKING_ENCRYPTION_KEY`) | Until disconnected or replaced, then erased | +| Synced bank transactions: date, amount, currency, description, counterparty name and IBAN, own IBAN, bank and account details | `bank_transactions.payload` | Encrypted (same key), one value per transaction | Until the user deletes them or disconnects the bank | +| Alongside each transaction: owner id, connection id, when it was stored, and an opaque keyed fingerprint used to skip repeats | `bank_transactions` | Plain (the fingerprint reveals nothing about the transaction) | Same as above | +| Sync history (when, which bank, how many, error kind) | `bank_sync_log` | Plain | 90 days | +| Audit trail (who did what, with emails) | `bank_audit_log` | Plain | 365 days | +| One-time sign-in states for connecting a bank | `bank_auth_states` | Plain | Ten minutes, purged on start-up | + +Not stored on the server: transactions imported from files (CSV, XLSX). They stay in the user's browser. + +## What is stored in the browser + +Parsed transactions and optionally the session (IndexedDB, "remember session"), categories, splits and exclusions the user chose. Deleting the server copy does not clear this. + +## Who else receives data + +- **Enable Banking** (bank connection provider): receives the user's choice of bank and returns account and transaction data under the user's own consent. FinKit never sees bank passwords. When a person presses Sync, their IP address and browser string are forwarded to the bank (through Enable Banking) so the bank can tell a person is present; FinKit does not store them. Scheduled syncs send neither. +- **Cloudflare Access**: signs people in and supplies their email address to the API. +- **Google Sheets**: only when the user exports; receives the monthly summary row they chose, into a spreadsheet they control. +- **The hosting provider**: runs the server and holds the database file and its backups. +- Nothing is sold, shared for advertising, or sent to analytics or crash-reporting services. + +## Bank consent + +Access is read-only and lasts the bank's allowed period (typically 90 to 180 days). The user can revoke it at any time by disconnecting. When consent runs out, syncing stops until they reconnect. Scheduled syncs run about twice a day while consent is active. + +## How people exercise their rights today + +- **See their data:** the Banks and Transactions pages; stored transactions are also returned by `GET /api/banking/transactions`. +- **Delete synced transactions:** Banks → Stored bank data, or disconnect the bank (removes what came through it). +- **Delete rules and settings:** Settings. +- **Withdraw consent:** disconnect the bank, which also revokes access at the bank. +- **Not available yet:** deleting a whole account (email, rules, settings, logs) and a downloadable export of all server-side data. Until built, these have to be done by the operator on request. + +## Access control + +Only people on `ALLOWED_USER_EMAILS` and signed in through Cloudflare Access can use the API. Every stored row carries its owner and every query filters by it. Connection secrets and transaction text are encrypted at rest. Backups of the database contain the same data and the same protection. + +## Open points to settle before publishing + +1. Legal basis and the controller's identity and contact details (for a self-hosted setup, that is whoever runs it). +2. Whether a fixed retention period for transactions is wanted (currently: until deleted or disconnected). +3. Build account deletion and data export, or state the manual process. +4. Backup retention at the host. +5. Where the server runs (country), for transfer wording. diff --git a/docs/users-and-access.md b/docs/users-and-access.md new file mode 100644 index 0000000..05ae935 --- /dev/null +++ b/docs/users-and-access.md @@ -0,0 +1,50 @@ +# Users and access + +## Who you are + +FinKit has no login of its own. In production, Cloudflare Access signs people in and attaches a signed token to every request. The API verifies the token itself (`CF_ACCESS_TEAM_DOMAIN`, `CF_ACCESS_AUD`) and reads the email from it. If those settings are missing when `NODE_ENV=production`, the server refuses to start. Locally there is no Cloudflare, so you are `dev@localhost`. `ALLOWED_ORIGIN` (the web app's address) is also required in production, and the server refuses to start without it. Changing requests must carry exactly that `Origin` header (no `Referer` fallback). Locally, with it unset, Origin isn't checked. + +## Who gets in + +One list: `ALLOWED_USER_EMAILS`. Anyone on it is let in. Anyone else is refused, even with a valid Cloudflare sign-in. + +- Everyone on the list is equal. There are no roles. +- The list is checked on every request, so taking someone off it locks them out at once. +- With nobody listed (outside local development) nobody gets in. The API logs this at start-up. +- `BANKING_ADMIN_EMAILS` is the old name for the same list and still works. + +## What each person sees + +Only their own: learned merchant rules, settings (including the Google Sheet layout), bank connections, and the Back office (their sync history, their activity, their connection counts). Asking for someone else's bank connection returns 404. Synced bank transactions are stored on the server, each row tagged with its owner. Every read and delete is limited to the signed-in user, and the descriptive fields (text, names, IBANs) are encrypted at rest. Disconnecting a bank deletes what came through it; Banks → Stored bank data deletes everything. Transactions from imported files are never sent to the server. + +The bank's return after approval carries no sign-in, so the connection belongs to whoever started it. + +## Upgrading an existing install + +Older databases kept one global list of rules, settings and connections. On first start of this version they stay unowned, and are handed to the first address in `ALLOWED_USER_EMAILS` the first time that person signs in, whatever order people arrive in. Put your own address first, and back up the database file before upgrading. + +## Setting it up + +``` +ALLOWED_USER_EMAILS=you@example.com,partner@example.com +CF_ACCESS_TEAM_DOMAIN=yourteam.cloudflareaccess.com +CF_ACCESS_AUD= +``` + +Also add each person to the Cloudflare Access policy. Cloudflare decides who may reach the site, and FinKit's list decides who gets an account. + +## Locked out? + +The email in the sign-in token isn't on `ALLOWED_USER_EMAILS`. Fix the variable and restart the API. If the old data went to the wrong person on first start, stop the API, restore the database backup, put the right address first, and start again. + +## Google Sheets export + +Export, layout and metadata calls take the spreadsheet id from the request, on purpose: any signed-in person on the allowed list may export to any spreadsheet the Google service account can open. So the service account must only be shared on the one spreadsheet FinKit should write to (the README's Google Sheets steps do this). Tab names and column letters are validated before they reach Google. + +## Not built yet + +Invitations, a user list and profile screen, preferences stored on the server, deleting one person's data. + +## For developers + +`X-Dev-User: someone@example.com` acts as another person in local development. It is ignored in production. diff --git a/docs/web-architecture.md b/docs/web-architecture.md new file mode 100644 index 0000000..700e706 --- /dev/null +++ b/docs/web-architecture.md @@ -0,0 +1,116 @@ +# Web frontend architecture + +Feature-based layout: **`app/`** wires the shell, **`features/`** own product areas, **`shared/`** holds cross-cutting code. Features may import from `shared/` only—not from other features. + +## File size + +Keep source files around **200 lines** or less (excluding generated assets). Split by component, hook, or pure module when a file grows beyond that. Vitest specs may live in several files under a feature’s `tests/` folder. + +## Explicit state machines + +Multi-step UI and async flows use **discriminated unions + reducers** (not scattered booleans). Prefer reading `workspacePhase`, `TableInteraction`, etc. over inferring from many flags. + +| Area | Types / module | +|------|----------------| +| Workspace restore & import | `shared/contexts/workspace/workspacePhase.ts` — `WorkspacePhase`, `SheetBootstrapPhase` | +| Empty-state file upload UI | `features/import/components/fileUploadPhase.ts` — `FileUploadUiPhase` | +| Review triage | `features/review/components/reviewQueuePhase.ts` — `ReviewQueuePhase` + session reducer | +| Transaction table modals | `features/transactions/components/tableInteraction.ts` — `TableInteraction` | +| Settings sheet async | `features/settings/components/settingsAsyncPhase.ts` — `SettingsAsyncOperation` | +| Bank OAuth return & card busy | `features/banking/bankingUiPhase.ts` — `BankConnectReturnPhase`, `ConnectionCardOperation` | + +`AppState` still exposes `isLoading`, `isRestoring`, and `error` as **derived** from `workspacePhase` for existing callers; prefer `workspacePhase` for new code. + +## Repository layout + +``` +web/src/ +├── app/ # wiring only +│ ├── main.tsx # entry, providers +│ ├── router.tsx # composes feature routes +│ ├── shell.tsx # root layout (nav, outlets; may import features for badges) +│ └── AppShellSidebar.tsx +├── features/ +│ ├── overview/ # dashboard +│ ├── transactions/ # list, merchant drill-down, table, filters +│ ├── import/ # file upload UI +│ ├── review/ # review queue +│ ├── income/ # income classification +│ ├── analytics/ # charts & trends routes +│ ├── export/ # Google Sheets preview +│ ├── settings/ # sheet config, rules, rates +│ └── banking/ # open banking UI, API hooks, admin back-office +├── shared/ +│ ├── components/ # layout (header, breadcrumbs), feedback (toast, errors) +│ ├── contexts/ # AppState provider +│ │ └── workspace/ # session store, import, mutations, bootstrap +│ ├── services/ # HTTP helpers (`API_BASE`, headers) +│ ├── types/ # shared TS types +│ └── utils/ # parsing, categorization, dates, storage, etc. +│ ├── parser/ # CSV/XLSX/OFX import (import via `@/shared/utils/parser` — thin `.ts` shim re-exports the folder) +│ ├── categorizer/ # rules + learned mappings +│ ├── anonymizer/ # optional PII masking +│ └── sheetLayout/ # Google Sheet column layout +└── styles/ + └── index.css +``` + +## Feature module shape + +Add folders **only when they contain code**. Do not scaffold empty `api/`, `hooks/`, `tests/`, etc. “just in case.” + +| Folder | Role | +|---------------|------| +| `routes/` | Route entry components; `index.tsx` re-exports the page | +| `components/` | Presentational UI; no data fetching | +| `hooks/` | TanStack Query / local logic (e.g. `banking/hooks`, `transactions/hooks`) | +| `api/` | Backend calls owned by this feature (**`export/api`** — Google Sheet layout/metadata; **`settings/api`** — sheet config & learned mappings; **`banking/api`** — open banking) | +| `constants/` | Search params, durations, domain numbers (see `constants.ts`; time units in `@/shared/constants/time`) | +| `utils/` | Pure helpers scoped to the feature | +| `tests/` | Vitest specs (e.g. `transactions/tests`) | + +Most features are UI over **`AppState`** and **`shared/utils`**; they need only `routes/` and `components/` unless you add feature-specific HTTP or hooks. + +## Workspace state (`AppState`) + +`shared/contexts/AppState.tsx` is a thin provider. Logic lives under `shared/contexts/workspace/`: + +| Module | Role | +|--------|------| +| `useTransactionStore` | Refs + `commit` for transactions/raw/splits | +| `useWorkspaceBootstrap` | Init, sheet sync, session restore | +| `useWorkspaceImport` | File upload, bank import, duplicate confirm | +| `useWorkspaceMutations` | Edits, splits, clear, CSV, anonymize toggle | +| `useRecategorizeMutations` | Category changes + undo | +| `useMappingPersistence` | Batch save learned mappings to API | + +## Known exceptions + +- `app/shell.tsx` imports banking (nav badges) and review helpers for counts. +- Settings back-office tab embeds a banking component when banking is enabled. +- Settings sheet tab uses **`export/api`** for Google metadata (same backend routes as export). +- **`shared/utils/sheetLayout`** calls **`export/api`** to sync header layout from Sheets. + +## Current route mapping + +| Feature | Routes | Notes | +|---------|--------|--------| +| `overview` | `/` | Summary cards, empty state import | +| `transactions` | `/transactions`, `/transactions/merchants/$merchantKey` | Core table & filters | +| `review` | `/review` | Review queue | +| `income` | `/income` | Income review | +| `analytics` | `/analytics`, `/analytics/trends` | Charts | +| `export` | `/export` | Sheet export preview | +| `settings` | `/settings/*` | Sheet, columns, rules | +| `banking` | `/banks/*` | Optional; gated when API banking is off | +| `import` | (embedded) | File upload from overview / shell | + +Shared **transaction pipeline** (parser, categorizer, anonymizer, splits, …) lives under `shared/utils/` because every feature reads the same workspace. Implementation modules live in subfolders (`categorizer/`, etc.); one-line **`*.ts` shims** next to them (e.g. `categorizer.ts`) re-export the folder so Vite and `@/` imports keep working. + +## Import alias + +Use `@/` for absolute imports (see `web/tsconfig.json`), e.g. `@/shared/types`, `@/features/banking/hooks/banking-queries`. + +## Entry + +The SPA entry is `web/app/index.html` → `/src/app/main.tsx`. The marketing landing page remains `web/index.html` at the repo root. diff --git a/example.env b/example.env deleted file mode 100644 index 2ae5026..0000000 --- a/example.env +++ /dev/null @@ -1,26 +0,0 @@ -# FinKit Environment Variables -# Copy this file to .env and fill in your values - -# Frontend API URL (optional - defaults to /api for same-origin) -# Set this if API is on a different domain -# VITE_API_URL=https://api.yourdomain.com - -# GitHub Container Registry authentication (for pulling images) -# Create a PAT at: https://github.com/settings/tokens -# Required scope: read:packages -GITHUB_USER=your-github-username -GITHUB_TOKEN=ghp_xxxxxxxxxxxxxxxxxxxx - -# Cloudflare Tunnel token (from Cloudflare Zero Trust dashboard) -CLOUDFLARE_TUNNEL_TOKEN=your-tunnel-token - -# OpenAI API Key (for AI categorization) - Optional -OPENAI_API_KEY=sk-... - -# Google Sheets API (for export functionality) - Optional -# Create a service account at https://console.cloud.google.com -GOOGLE_SERVICE_ACCOUNT_EMAIL=your-service-account@project.iam.gserviceaccount.com -GOOGLE_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n" - -# Lock the API's CORS to your site (optional; recommended in production) -# ALLOWED_ORIGIN=https://finkit.example.com diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..9bf82e7 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,332 @@ +{ + "name": "finkit", + "version": "2.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "finkit", + "version": "2.0.0", + "devDependencies": { + "concurrently": "9.2.5" + }, + "engines": { + "node": ">=26.13.0" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concurrently": { + "version": "9.2.5", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.5.tgz", + "integrity": "sha512-YMZlvWrTPQEhjKp60XSmMGt60+pffbv6DyRnOMvVO98+J7aF8w8Ui9FhdcOwvSixc/kyR6SoVvmejcn/2lZBqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "4.1.2", + "rxjs": "7.8.2", + "shell-quote": "1.12.0", + "supports-color": "8.1.1", + "tree-kill": "1.2.2", + "yargs": "17.7.2" + }, + "bin": { + "conc": "dist/bin/concurrently.js", + "concurrently": "dist/bin/concurrently.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/shell-quote": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.12.0.tgz", + "integrity": "sha512-PcByqNyT/38F2kDNi006HAMRJaULuBzq/FOsw3qdZvX/GA9W/jamDaRskgHjubHiftXK5sIFxLNkvrXUwcof6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/tslib": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.9.1.tgz", + "integrity": "sha512-a4Ux9VKuIMHsOS7ENbEq6hK2ZGYIT27Rtl0E4eD9KqwxhKiQeMZE/9xb3IdYwWaSOyIBQLExhQgXDTYoiCxnWA==", + "dev": true, + "license": "0BSD" + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..ada15ee --- /dev/null +++ b/package.json @@ -0,0 +1,24 @@ +{ + "name": "finkit", + "private": true, + "version": "2.0.0", + "scripts": { + "install:all": "npm install --prefix api && npm install --prefix web", + "dev": "concurrently -n api,web -c blue,green \"npm run dev:api\" \"npm run dev:web\"", + "dev:api": "npm run dev --prefix api", + "dev:web": "npm run dev --prefix web", + "build": "npm run build --prefix api && npm run build --prefix web", + "test": "npm run test --prefix api && npm run test --prefix web", + "lint": "npm run lint --prefix api && npm run lint --prefix web", + "lint:fix": "npm run lint:fix --prefix api && npm run lint:fix --prefix web", + "format": "npm run format --prefix api && npm run format --prefix web", + "format:check": "npm run format:check --prefix api && npm run format:check --prefix web", + "check": "npm run check --prefix api && npm run check --prefix web" + }, + "devDependencies": { + "concurrently": "9.2.5" + }, + "engines": { + "node": ">=26.13.0" + } +} diff --git a/web/.oxfmtrc.json b/web/.oxfmtrc.json new file mode 100644 index 0000000..7609490 --- /dev/null +++ b/web/.oxfmtrc.json @@ -0,0 +1,9 @@ +{ + "$schema": "./node_modules/oxfmt/configuration_schema.json", + "printWidth": 120, + "semi": false, + "singleQuote": true, + "trailingComma": "all", + "arrowParens": "avoid", + "ignorePatterns": ["dist", "node_modules", "package-lock.json", "tsconfig.tsbuildinfo", "*.html", "public"] +} diff --git a/web/.oxlintrc.json b/web/.oxlintrc.json new file mode 100644 index 0000000..4ee2e07 --- /dev/null +++ b/web/.oxlintrc.json @@ -0,0 +1,54 @@ +{ + "$schema": "./node_modules/oxlint/configuration_schema.json", + "jsPlugins": ["@stylistic/eslint-plugin"], + "plugins": ["typescript", "unicorn", "oxc", "react", "jsx-a11y", "import", "vitest"], + "categories": { + "correctness": "error", + "suspicious": "warn" + }, + "rules": { + "curly": ["error", "all"], + "@stylistic/max-statements-per-line": ["error", { "max": 1 }], + "@stylistic/padding-line-between-statements": [ + "error", + { "blankLine": "always", "prev": "*", "next": "return" }, + { "blankLine": "always", "prev": ["const", "let", "var"], "next": "*" }, + { "blankLine": "any", "prev": ["const", "let", "var"], "next": ["const", "let", "var"] } + ], + "@stylistic/lines-between-class-members": [ + "error", + { + "enforce": [{ "blankLine": "always", "prev": "*", "next": "method" }] + } + ], + "react/rules-of-hooks": "error", + "react/exhaustive-deps": "warn", + "react/react-in-jsx-scope": "off", + "jsx-a11y/prefer-tag-over-role": "off", + "jsx-a11y/no-autofocus": "off", + "unicorn/no-array-sort": "off", + "unicorn/prefer-add-event-listener": "off", + "import/no-unassigned-import": [ + "warn", + { + "allow": ["**/*.css"] + } + ], + "react/set-state-in-effect": "warn", + "react/purity": "warn", + "react/immutability": "warn" + }, + "env": { + "builtin": true, + "browser": true + }, + "ignorePatterns": ["dist", "node_modules"], + "overrides": [ + { + "files": ["**/*.test.ts", "**/*.test.tsx"], + "rules": { + "unicorn/consistent-function-scoping": "off" + } + } + ] +} diff --git a/web/Dockerfile b/web/Dockerfile index 3cf1169..05ed6e2 100644 --- a/web/Dockerfile +++ b/web/Dockerfile @@ -1,7 +1,7 @@ # ============================================ # Dependencies stage # ============================================ -FROM node:20-alpine AS deps +FROM node:26-alpine AS deps WORKDIR /app @@ -14,7 +14,7 @@ RUN npm ci # ============================================ # Build stage # ============================================ -FROM node:20-alpine AS builder +FROM node:26-alpine AS builder WORKDIR /app @@ -33,7 +33,7 @@ RUN test -f dist/index.html && test -f dist/app/index.html # ============================================ # Production stage # ============================================ -FROM node:20-alpine AS production +FROM node:26-alpine AS production WORKDIR /app diff --git a/web/app/index.html b/web/app/index.html index 06f45c7..1087736 100644 --- a/web/app/index.html +++ b/web/app/index.html @@ -12,6 +12,6 @@
- + diff --git a/web/index.html b/web/index.html index f10148b..60157c5 100644 --- a/web/index.html +++ b/web/index.html @@ -22,8 +22,8 @@
-

A private dashboard for one person's bank accounts

-

FinKit reads the owner's own bank transactions, sorts them into categories, and prepares the monthly figures for a budget spreadsheet. It is a personal tool, not a service for the public.

+

A private, self-hosted dashboard for your bank accounts

+

FinKit runs on infrastructure you control. It reads your transactions (file import or optional bank sync), categorizes them in your browser, and can prepare monthly figures for a spreadsheet you own. No FinKit cloud, no telemetry, and no data sold or shared—unless you turn on optional Google Sheets or bank sync yourself.

Sign in Privacy @@ -31,9 +31,10 @@

A private dashboard for one person's bank accounts

-

Read-only

It can see account names, balances and transactions. It cannot make payments or move money.

-

Kept private

Transactions are shown in the owner's browser and are not kept on the server. Bank logins never reach FinKit.

-

Owner access only

Signing in is limited to the owner. There are no public accounts or sign-ups.

+

Self-hosted

Web app and API run on your machine or server. SQLite rules and tokens stay on your disk; transactions stay in your browser unless you export.

+

No data gathering

No analytics, ads, or crash reports built into FinKit. Nothing is sent to the software authors by default.

+

Read-only banking

With sync enabled, it can fetch accounts and transactions. It cannot move money. Bank passwords never reach FinKit.

+

Optional export only

Transactions are not uploaded elsewhere unless you configure Google Sheets export or open-banking sync.

diff --git a/web/package-lock.json b/web/package-lock.json index 112cfb9..c6584e4 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -8,25 +8,33 @@ "name": "finkit-web", "version": "2.0.0", "dependencies": { - "lucide-react": "^0.577.0", - "papaparse": "^5.7.0", - "react": "^18.3.1", - "react-dom": "^18.3.1", - "recharts": "^2.15.4", - "xlsx": "^0.18.5" + "@tanstack/react-query": "5.105.1", + "@tanstack/react-router": "1.170.46", + "lucide-react": "0.577.0", + "papaparse": "5.7.0", + "react": "19.3.1", + "react-dom": "19.3.1", + "recharts": "2.15.4", + "xlsx": "0.18.5" }, "devDependencies": { - "@types/node": "^22.10.0", - "@types/papaparse": "^5.5.2", - "@types/react": "^18.3.31", - "@types/react-dom": "^18.3.7", - "@vitejs/plugin-react": "^4.7.0", - "autoprefixer": "^10.6.2", - "postcss": "^8.5.32", - "tailwindcss": "^3.4.19", - "typescript": "~5.6.2", - "vite": "^6.4.5", - "vitest": "^2.1.9" + "@stylistic/eslint-plugin": "4.4.1", + "@types/node": "24.19.2", + "@types/papaparse": "5.5.2", + "@types/react": "19.3.0", + "@types/react-dom": "19.3.0", + "@vitejs/plugin-react": "4.7.0", + "autoprefixer": "10.6.2", + "oxfmt": "0.75.0", + "oxlint": "1.90.0", + "postcss": "8.5.32", + "tailwindcss": "3.4.19", + "typescript": "6.0.3", + "vite": "6.4.5", + "vitest": "3.2.7" + }, + "engines": { + "node": ">=26.13.0" } }, "node_modules/@alloc/quick-lru": { @@ -333,6 +341,32 @@ "node": ">=6.9.0" } }, + "node_modules/@cacheable/memory": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@cacheable/memory/-/memory-2.2.0.tgz", + "integrity": "sha512-CTLKqLItRCEixEAewD3/j9DB3/o96gpTPD4eJ1v+DGOlxZRZncRQkGYqqnAGCscYd6RNeXfGeiuCphsPtqyIfQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@cacheable/utils": "^2.5.0", + "@keyv/bigmap": "^1.3.1", + "hookified": "^1.15.1", + "keyv": "^5.6.0" + } + }, + "node_modules/@cacheable/utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@cacheable/utils/-/utils-2.5.0.tgz", + "integrity": "sha512-buipgOVDkkPXNR5+xBpDw7Zk2n1EvU7qBJCNUcL7rhQ//kfpOXPAvQ511Os0vpLYJ1pZnvudNytkQt2hst3wqA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "hashery": "^1.5.1", + "keyv": "^5.6.0" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.12", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", @@ -775,6 +809,190 @@ "node": ">=18" } }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", + "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.23.5", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", + "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@eslint/object-schema": "^3.0.5", + "debug": "^4.3.1", + "minimatch": "^10.2.4" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.7.0.tgz", + "integrity": "sha512-DObd/KKUsU+FaFv4PLxSRenpXfQWmPXXP3pPZ6/K1PCrMu2vQpMDMuQe/BqYeoLcz8ro0bVDF1RxOJgfVEdhUw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@eslint/core": "^1.2.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/core": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/object-schema": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", + "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.3.tgz", + "integrity": "sha512-IkO+/KEUvwbVpiURZg+P7zF74z5Jxe0UgJxVni+RtoHQ6IZieXaO02kmadomap/q+l6bc/jdPGGqTjhuZnuz1Q==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@eslint/core": "^1.2.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -825,6 +1043,32 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@keyv/bigmap": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@keyv/bigmap/-/bigmap-1.3.1.tgz", + "integrity": "sha512-WbzE9sdmQtKy8vrNPa9BRnwZh5UF4s1KTmSK0KUVLo3eff5BlQNNWDnFOouNpKfPKDnms9xynJjsMYjMaT/aFQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "hashery": "^1.4.0", + "hookified": "^1.15.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "keyv": "^5.6.0" + } + }, + "node_modules/@keyv/serialize": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.1.1.tgz", + "integrity": "sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/@napi-rs/lzma-linux-x64-gnu": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", @@ -880,17 +1124,10 @@ "node": ">= 8" } }, - "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-beta.27", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", - "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.67.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.67.4.tgz", - "integrity": "sha512-/v/pUAPOcwp+r1Hjjdg0cBV1TE89CzGKrKh2FgkIQw2QBcIMmVPOCSfuOZStCn4Xh/98HXfo/ht6ztcssctPNg==", + "node_modules/@oxfmt/binding-android-arm-eabi": { + "version": "0.75.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-android-arm-eabi/-/binding-android-arm-eabi-0.75.0.tgz", + "integrity": "sha512-zHJ7LcTN46/5mtoLxsUu14pdF9ixFwDURtmxZ0CeNwqom5qB5NRVqU6o2GRZ7QPdNBW8G6j9UTJzQ+EDevMH0w==", "cpu": [ "arm" ], @@ -899,12 +1136,15 @@ "optional": true, "os": [ "android" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.67.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.67.4.tgz", - "integrity": "sha512-6tmmB/P52Vt167FXXEw5auiEUgTtZz7aqUgWHqYXToIUE0HFDY6fTdAyeekjjsNk6O/8dtpSKtClxSj1mMneKQ==", + "node_modules/@oxfmt/binding-android-arm64": { + "version": "0.75.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-android-arm64/-/binding-android-arm64-0.75.0.tgz", + "integrity": "sha512-IdxsKZJvHZUvI7q6kLmzXdEFpFiBEDQw/WJtSp1QlzNXBw1JlLN3tY50WwWd4doPnq86XW4P+7BsQwu6meCW+w==", "cpu": [ "arm64" ], @@ -913,26 +1153,15 @@ "optional": true, "os": [ "android" - ] - }, - "node_modules/@rollup/rollup-android-x64": { - "version": "4.67.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-x64/-/rollup-android-x64-4.67.4.tgz", - "integrity": "sha512-8Z9Q4RmotVmuL4llOvqX814JNiOK1oqoBv+glG5KbpRfUiypqzz2/fxIzK4Pq129l4dT5a8Gszmpc0/XPYoFuQ==", - "cpu": [ - "x64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.67.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.67.4.tgz", - "integrity": "sha512-pnq6hssxTtwuRAl6Kdi/+IDY+b1IhkUetjhUkdLAAJCssjfqtL2Oqy/FXJVJFtAWOu1oRlArIvf0Yk1O92x2HQ==", + "node_modules/@oxfmt/binding-darwin-arm64": { + "version": "0.75.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-darwin-arm64/-/binding-darwin-arm64-0.75.0.tgz", + "integrity": "sha512-vyiqZilO5jal/nXZ0143NyKcP6TkzjaQdVEWU/kwgyWqugAgIRgAbrnO3MOF+e6LkQu3CrtrkPoc3mrsZZY6cA==", "cpu": [ "arm64" ], @@ -941,12 +1170,15 @@ "optional": true, "os": [ "darwin" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.67.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.67.4.tgz", - "integrity": "sha512-BeIrGZvHMVdJg/N+ohZfFywG/gF41684YhDj2wBvouj7m9ziP1tZDUNwzXRiDY+3WIKngaC4hiUBqVtBPGP5og==", + "node_modules/@oxfmt/binding-darwin-x64": { + "version": "0.75.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-darwin-x64/-/binding-darwin-x64-0.75.0.tgz", + "integrity": "sha512-oua7eVqVe769mDam6ORcavygxTzG3ovo5QRQ1kJCPaEbnWXPYhtAiYDj8pajVmFjO7j8Elf3gInnWpTX0LLYtg==", "cpu": [ "x64" ], @@ -955,40 +1187,49 @@ "optional": true, "os": [ "darwin" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.67.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.67.4.tgz", - "integrity": "sha512-fRFX/oqNg/g780DKoS8RIfYcZD59Ru5O/bSeK1N7gQljBFrTxJY/q5EX4eMs5SrJf32VgVBVVYCdfPpzAwPPnQ==", + "node_modules/@oxfmt/binding-freebsd-x64": { + "version": "0.75.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-freebsd-x64/-/binding-freebsd-x64-0.75.0.tgz", + "integrity": "sha512-NWkbezhg7mx2Ep94g/TysR3c1bsISsU4lQ6t5xI4841PAk0Kqcn1+UEe+BoFH2/UK2YegPXq+OF+0bUcSvUalA==", "cpu": [ - "arm64" + "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "freebsd" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.67.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.67.4.tgz", - "integrity": "sha512-bSrBRopG1q+0RuOsW0NtYTtIODlvNA3T3ebx5WnQVdSxWIoXFmj+rZdHUHdSLgFdZ5M6VGmekYE7r4RMGQXjfg==", + "node_modules/@oxfmt/binding-linux-arm-gnueabihf": { + "version": "0.75.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.75.0.tgz", + "integrity": "sha512-CAHX87kJplTq2E0OWP7opG8gEVlHc7CeLEi4nMJOyx3psuqBKf84HZ3UIkX2guWEJ5UljU2zJF0o46XylywVVA==", "cpu": [ - "x64" + "arm" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "freebsd" - ] + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.67.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.67.4.tgz", - "integrity": "sha512-L8IvdrJVtl65g1ozwDSyjGPG+FmibhCdFUwmqTItJLw0S3+Oj4QzFs7rfgTUiK7rsPjkCLdG1/GApFxoynXFJQ==", + "node_modules/@oxfmt/binding-linux-arm-musleabihf": { + "version": "0.75.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.75.0.tgz", + "integrity": "sha512-jmxYOiQzx2fp+OwQb7717K1zI8AMKEykw04sfr95/mU8SIL/oKgQq8nXH42sEznEquBL2RU7nLY36eBnYFT+vg==", "cpu": [ "arm" ], @@ -997,26 +1238,32 @@ "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.67.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.67.4.tgz", - "integrity": "sha512-h7PcoQOzC31jshNpQ48J0akB9tsghjkSjSl5AZ6XsVGTxVsln7t7C4SQyw62tNVSaz2SeHgk4VT2ELbL17BSHQ==", + "node_modules/@oxfmt/binding-linux-arm64-gnu": { + "version": "0.75.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.75.0.tgz", + "integrity": "sha512-eimaiAhx08dbOg12Fj3PYhymXY8vRbaiJB505vDXMy6ohne3Y1qTS0e5KEwFMq3LmDjEO+16BX38c/oXFI/SAQ==", "cpu": [ - "arm" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.67.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.67.4.tgz", - "integrity": "sha512-KSPuOBNHoOl7AsVjeY1KZci8+0up0U4TFAHU5IzBYN8P+Z6cB9DZq28+JQycZW7f6WLLa/tSzhV7Rdy95bVi+g==", + "node_modules/@oxfmt/binding-linux-arm64-musl": { + "version": "0.75.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.75.0.tgz", + "integrity": "sha512-bjSONIG1ba2xugr9oHQau0E6oPFYOMjPaa5hK/wpVC/Q1pcgf2BZMKwbuA8/cWzhWX5QV/UdPzD+rw1kQHpikQ==", "cpu": [ "arm64" ], @@ -1025,138 +1272,168 @@ "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.67.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.67.4.tgz", - "integrity": "sha512-0E/mKmVk1PlQeQYygIwQm0jlwjANTzGSiNBMPrFx+rdFGFj/2Axh4p6XwcrystQIqXZVudTaGsdbDpzYgJ4wSA==", + "node_modules/@oxfmt/binding-linux-ppc64-gnu": { + "version": "0.75.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.75.0.tgz", + "integrity": "sha512-wDksWQXC/KZucISyqlhBtvpW+pyHh8z0Cjm6Qxiv22XlllM2Bx5gJGDnPYjV+H0lL+o1iqLlEli8jmBuRCLcjw==", "cpu": [ - "arm64" + "ppc64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.67.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.67.4.tgz", - "integrity": "sha512-Fvw7dOSc2Sa9UOipRZVI0zOgdLCzsjVyD/6JuHXzUHTXFu7mMgmeX/0/bCVBIut1dz+nWz3pV4WpS/Qg0XVnxw==", + "node_modules/@oxfmt/binding-linux-riscv64-gnu": { + "version": "0.75.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.75.0.tgz", + "integrity": "sha512-5JkNwViqaVuk4I+9+zgVrEYiHOAPXFxgDgP6iDnpdOBjm7XAHAOjSvolHZZAokWPx7CGOw8tKteXmn4WPIInaQ==", "cpu": [ - "loong64" + "riscv64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.67.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.67.4.tgz", - "integrity": "sha512-jzxbVHaE7V3F+P0wEK7UydQtvv7Twfdtx58jI5/3VRx1jJxyrn+qFwfXmqjVFxPhBl+zjaRs+ZJMFJw7vdHaQA==", + "node_modules/@oxfmt/binding-linux-riscv64-musl": { + "version": "0.75.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.75.0.tgz", + "integrity": "sha512-1EVqRIE0eZLB08j3ZCVN47Ors7LCXGmOI+3xJYgyoFUKUerD3Glxb+Q1Bp3lK8NeT5R3ZdS9uhX/wobNBYZ6jw==", "cpu": [ - "loong64" + "riscv64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.67.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.67.4.tgz", - "integrity": "sha512-fhtrzp9p4GjEJVaigaap9g6X9nP6xgJtKiLLXSDgpE18HXufA5aqfltGn+KAqMtVZReJZfOrXm//y5YSA9BWbA==", + "node_modules/@oxfmt/binding-linux-s390x-gnu": { + "version": "0.75.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.75.0.tgz", + "integrity": "sha512-UJWRVNT6Yuw4ssLv2BwaBUWU1GWtByE+EnoHu8I+P1t8Kn+WozYU5WYOqb5aj8gbXJJ8gGZt+w40Hi/7L6iVLw==", "cpu": [ - "ppc64" + "s390x" ], "dev": true, "license": "MIT", "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.67.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.67.4.tgz", - "integrity": "sha512-uynaF7IUHwAckV0S+yC1f5esy9gB+aqCBsPH5BOck32TYdCuAn2lVG5tvUcFnEsrviGEmRZxE1NHSOieUPYJEg==", + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-x64-gnu": { + "version": "0.75.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.75.0.tgz", + "integrity": "sha512-FHdvHhyz8ChBctBUvVGti49kSEwANd1PZQwlXUEoF9Ed4plxXxk/wWc0mfZmrze2lRgmME7ytDmVIOixYauQhg==", "cpu": [ - "ppc64" + "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.67.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.67.4.tgz", - "integrity": "sha512-uaqRFdfb8eIa2bVHN/FE3ndCvR0zV2xv0AwjCfxrqSu6l6fp/54VhjfhslKlf2RIik9L1jwlt4E56PeVX6tQtQ==", + "node_modules/@oxfmt/binding-linux-x64-musl": { + "version": "0.75.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-x64-musl/-/binding-linux-x64-musl-0.75.0.tgz", + "integrity": "sha512-6MjtsF96s9mSBV0GEwUe4pOFPP2Mz7ahcHPAit0DSKL5fM6oMM7RG1zvYxwB7+SBkIxsWBRhhxuqmy7pibNwsA==", "cpu": [ - "riscv64" + "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.67.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.67.4.tgz", - "integrity": "sha512-JZRow7kmxUDEASnxHuVG5itTm+nTXi9BQUABcVUnUEngC+OPwMrpx1uJ9hIA/v+IcAEBL52ktCSPXk5OJn9Jqg==", + "node_modules/@oxfmt/binding-openharmony-arm64": { + "version": "0.75.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-openharmony-arm64/-/binding-openharmony-arm64-0.75.0.tgz", + "integrity": "sha512-1D6T6aL1Pj+U5GLxqwV1fKjUJ5+FFm+bn7UrqNDMveZmWs0jo/3vc+dzzuV/jF/FdVo12gowODk3SnAO2YOwLg==", "cpu": [ - "riscv64" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" - ] + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.67.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.67.4.tgz", - "integrity": "sha512-rdrh4e+Lq9fCNKoDPaCqeHXbQCe7yl77Ukl45tlSClwYdZ1gpFGZz1XLJdYtY++FRlfL5aHvb6/0n3GDW/Nxsw==", + "node_modules/@oxfmt/binding-win32-arm64-msvc": { + "version": "0.75.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.75.0.tgz", + "integrity": "sha512-RX2eUmy7J/nM8+tnM3nzR8t1eqkwj1q8Kgo7beA+4DVgKYYHHyrEa3qEmthxw7Z+HqpCCEOWuoqa+jelo9/w2Q==", "cpu": [ - "s390x" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" - ] + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.67.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.67.4.tgz", - "integrity": "sha512-wL7MS4r0fYcxluF3ne2plofHZiB/LsqNCnyaUlyiKpAJLb6agPSFz7DEFCfkonWAZXMrLwCmC08b2ZEnEdJkog==", + "node_modules/@oxfmt/binding-win32-ia32-msvc": { + "version": "0.75.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.75.0.tgz", + "integrity": "sha512-UpKta0iSZmPLII0dYXYdbzdBTsGREUbSHLqgLdtKcqBvs40qjmUz+cgUcaU1smPx13DfbEG4XqnaSn5MoFN0og==", "cpu": [ - "x64" + "ia32" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" - ] + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.67.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.67.4.tgz", - "integrity": "sha512-4H0XHfd2VufGjX7NjUyeue78sD0bRIoKSGjGqO+F5nSvL6jN48NAU7esdmtFEYxZXwObLtt3MJ/kHS0jASMW3g==", + "node_modules/@oxfmt/binding-win32-x64-msvc": { + "version": "0.75.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.75.0.tgz", + "integrity": "sha512-pjiLiCk0QMCWxs0n5XiNU85YLAKA+wJxPjxlDVygmr4bP4glcfTU2nOpso5nR4Rdi2Pxv5YwakD4UE+eh7M0BQ==", "cpu": [ "x64" ], @@ -1164,27 +1441,33 @@ "license": "MIT", "optional": true, "os": [ - "linux" - ] + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.67.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.67.4.tgz", - "integrity": "sha512-osRHQbE55pZpCLetpMsFmhkapyZnAUH73qRKMh+npi3boUom6u6hdaTTPSxlInXxwJHsn9r2uDhdz0V7Yoxx2A==", + "node_modules/@oxlint/binding-android-arm-eabi": { + "version": "1.90.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.90.0.tgz", + "integrity": "sha512-sgfalb1HuxYjiAa4wESZ626z1FGusqNcXYHyB5oIOEprS2sbPg0UYEfo83uF1GNG70DIfZxHkXuE6KFrlSdn8Q==", "cpu": [ - "x64" + "arm" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "openbsd" - ] + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.67.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.67.4.tgz", - "integrity": "sha512-95+exYEyOwrZePsE0czi4q7X5lxKPa21+lmyAZlwbYENITQ0Unk4HAQAFp/p+KhXBHBx9mUABXRYjOC5A1wY3Q==", + "node_modules/@oxlint/binding-android-arm64": { + "version": "1.90.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm64/-/binding-android-arm64-1.90.0.tgz", + "integrity": "sha512-RcKhz1ETMq+fazE9TKS2+5NIRGwigm/nW8egqF0LI+A8Yw2iW5r0E/C3XAPRiwIm/vcPiutIRIa6IQCIKhZsTQ==", "cpu": [ "arm64" ], @@ -1192,13 +1475,16 @@ "license": "MIT", "optional": true, "os": [ - "openharmony" - ] + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.67.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.67.4.tgz", - "integrity": "sha512-FOXOcKhNQpO4Po+Ynan6IbtM67SrXZbgpRJWVYwz53CT5veI7sVzDwVBmWS65N5Imv+wK3SVhIPAl+C5DXXNCg==", + "node_modules/@oxlint/binding-darwin-arm64": { + "version": "1.90.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-arm64/-/binding-darwin-arm64-1.90.0.tgz", + "integrity": "sha512-PSrxMvP6mx+p1karZcUxLdy9lpaP+JWQPti4al9FgTaPhxE+pT/BjhGExloVVSWgZgIfOlLOjVyyq8BXlodhLQ==", "cpu": [ "arm64" ], @@ -1206,27 +1492,33 @@ "license": "MIT", "optional": true, "os": [ - "win32" - ] + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.67.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.67.4.tgz", - "integrity": "sha512-cklKh0gJOkAZQcWziQa3hrjxZKN+PCHZ7SPTyGJDAiBVr05ZXlEjGFCG7zFJMaVibxsmzAe1FimN4u/HBns9+Q==", + "node_modules/@oxlint/binding-darwin-x64": { + "version": "1.90.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-x64/-/binding-darwin-x64-1.90.0.tgz", + "integrity": "sha512-OLbU9qsrV2Q8QxT2OEzOPdO8zCCpyI8qB+hDI8yr6RsYELtkf0FAZhJdfjM/RnLHKzt44HWwAPnk92vBtKofvw==", "cpu": [ - "ia32" + "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "win32" - ] + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.67.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.67.4.tgz", - "integrity": "sha512-AegwxJPc9bvUp8fQtlsUHkFMOMkGylWU7glyL+YD4vwG+QaYoGXYLSsQoeTD7kvoLZrVlMWNVhTg1dedY5C1VQ==", + "node_modules/@oxlint/binding-freebsd-x64": { + "version": "1.90.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-freebsd-x64/-/binding-freebsd-x64-1.90.0.tgz", + "integrity": "sha512-fBA4pWyVDARmNucQqpyFuPC49xYf1r+sX7dxPL5fNPQrSMLum39Sn56tZ9JPleWC7FzLPwSQ7plYYfirYBHF8A==", "cpu": [ "x64" ], @@ -1234,342 +1526,1351 @@ "license": "MIT", "optional": true, "os": [ - "win32" - ] + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.67.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.67.4.tgz", - "integrity": "sha512-RJZnrwTWj7Iq2ooBwoGbDdC65tDwYawneGcAfFMJqVUrRZMcskka6nTkBpXR/47G7kzn3EW2qupExnaBy11vwg==", + "node_modules/@oxlint/binding-linux-arm-gnueabihf": { + "version": "1.90.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.90.0.tgz", + "integrity": "sha512-zhxH78eP2nR6/pGdhBc7MIfjhhcgATeu8fkmkFJbuzfk6Cj0FzBxyEhL85yDqrCybjx+G2YDzZGsxIO9kvO4Iw==", "cpu": [ - "x64" + "arm" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "win32" - ] + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@types/babel__core": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", - "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "node_modules/@oxlint/binding-linux-arm-musleabihf": { + "version": "1.90.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-1.90.0.tgz", + "integrity": "sha512-MvYmpHyJqUJjEvkZbJHdwfdL45cGn8EVBkYmoXFiaYFW9suoD3dbJl5mBdpy0DVgdjlyddmldbRtk3cCgETadQ==", + "cpu": [ + "arm" + ], "dev": true, "license": "MIT", - "dependencies": { - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@types/babel__generator": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", - "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "node_modules/@oxlint/binding-linux-arm64-gnu": { + "version": "1.90.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.90.0.tgz", + "integrity": "sha512-L13Sy+Kg0nZhwTsToniqjUFPA2TuM6962qCZ5Wp5KeB517Tw7ApdLTTtlb+bF86fcZZa2EtvCVvAJxk4Dya/MQ==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@babel/types": "^7.0.0" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@types/babel__template": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", - "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "node_modules/@oxlint/binding-linux-arm64-musl": { + "version": "1.90.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.90.0.tgz", + "integrity": "sha512-wFDdZWGpzIPFdJFjSLWnKB0TGzFriTKUIrl04iMqMyM9BBIJTuerfCHYa0WpDSjr5x3lcW/CHLyacSt886WpGg==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@types/babel__traverse": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", - "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "node_modules/@oxlint/binding-linux-ppc64-gnu": { + "version": "1.90.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.90.0.tgz", + "integrity": "sha512-FRywfn+MoKn8xpBSzgNwNVliSKRI2QXkxi/iF7o6FEQs8Bh8z2RpVTaFAcO3pchXjvB2eGNFIoLLQABIQ+u7VA==", + "cpu": [ + "ppc64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@babel/types": "^7.28.2" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@types/d3-array": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", - "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", - "license": "MIT" - }, - "node_modules/@types/d3-color": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", - "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", - "license": "MIT" - }, - "node_modules/@types/d3-ease": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", - "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", - "license": "MIT" - }, - "node_modules/@types/d3-interpolate": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", - "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "node_modules/@oxlint/binding-linux-riscv64-gnu": { + "version": "1.90.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-1.90.0.tgz", + "integrity": "sha512-IS2PaUgpc/qeLADTyhUNmdx3x4ijs9pjJdPKBYHaT+mAE7k3FsjnHPTjhpsak1b++h6CIPhv6WX9Z+y7Pw0voQ==", + "cpu": [ + "riscv64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "@types/d3-color": "*" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@types/d3-path": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", - "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", - "license": "MIT" - }, - "node_modules/@types/d3-scale": { - "version": "4.0.9", - "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", - "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "node_modules/@oxlint/binding-linux-riscv64-musl": { + "version": "1.90.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-1.90.0.tgz", + "integrity": "sha512-s7JFKRkGS8O7i2ZVODfuO++NC7sH6HDqpMn8lYxw91w0SXJOxnAVjQ8utvmDwLMMT0jaSkuOKuUYJDGDtu4sHQ==", + "cpu": [ + "riscv64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "@types/d3-time": "*" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@types/d3-shape": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.2.0.tgz", - "integrity": "sha512-kVd74ta9eof3eJOvbNd1vGKS/XERRyQbT26Og63hIsvDO84cjD5gEOhsXf26w3FSoNlPVz84DOFcKv/oou+fMw==", - "license": "MIT", - "dependencies": { - "@types/d3-path": "*" - } - }, - "node_modules/@types/d3-time": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", - "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", - "license": "MIT" - }, - "node_modules/@types/d3-timer": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", - "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", - "license": "MIT" - }, - "node_modules/@types/estree": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.10.tgz", - "integrity": "sha512-lTyOMnOcmRBzdCv6/4zUZCrEhV0xrqQiUZBZtVcbmLHBXKLnN8q89v/wKRF/Q0HbwVsqbIG3o5HDoFl9sduM+g==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "22.20.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.5.tgz", - "integrity": "sha512-U2+DNr+wSjpsTS/wZGYHq7GcwfuSmKiKvoPvK22zwTlRhU91yOniN4qRR5KhIjvif7ysw/dz/hKmfDH0Ris4aA==", + "node_modules/@oxlint/binding-linux-s390x-gnu": { + "version": "1.90.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.90.0.tgz", + "integrity": "sha512-b0GxrOQr5VONCkd7KLSqmP6b6nqZuTpuS8QPQHzltmqdXP5bY0Jl0CZ0QmlCTrS3ExaLYbioFYOARzNbYvcApQ==", + "cpu": [ + "s390x" + ], "dev": true, "license": "MIT", - "dependencies": { - "undici-types": "~6.21.0" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@types/papaparse": { - "version": "5.5.2", - "resolved": "https://registry.npmjs.org/@types/papaparse/-/papaparse-5.5.2.tgz", - "integrity": "sha512-gFnFp/JMzLHCwRf7tQHrNnfhN4eYBVYYI897CGX4MY1tzY9l2aLkVyx2IlKZ/SAqDbB3I1AOZW5gTMGGsqWliA==", + "node_modules/@oxlint/binding-linux-x64-gnu": { + "version": "1.90.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.90.0.tgz", + "integrity": "sha512-iaJeShJtgUp8rZbt9YUdTAGOpmX5KogCLba17Jp1tWdJ3F4M3D2g2TKUyI1FMTHxKH9NNHuQcuC2kZRg6IV7yQ==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@types/node": "*" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@types/prop-types": { - "version": "15.7.15", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", - "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/react": { - "version": "18.3.31", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.31.tgz", - "integrity": "sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==", + "node_modules/@oxlint/binding-linux-x64-musl": { + "version": "1.90.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-musl/-/binding-linux-x64-musl-1.90.0.tgz", + "integrity": "sha512-/aZoXnL5WBLCXuzfHqvEFXcm4iwYmr/cYByHDTXzLFzJdE5DC7bm6phpWqHv6BeqpVgY9IpBAxGDv2WEkl5JCg==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@types/prop-types": "*", - "csstype": "^3.2.2" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@types/react-dom": { - "version": "18.3.7", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", - "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "node_modules/@oxlint/binding-openharmony-arm64": { + "version": "1.90.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-openharmony-arm64/-/binding-openharmony-arm64-1.90.0.tgz", + "integrity": "sha512-SMhk7ozwoxYDKfYqGZCQEfULOKDneF9uhaoIf2neoDLWvNtjY9yVeL7YHmE5L9WdKvMpWNwwZxhenxBu6AgfOg==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "peerDependencies": { - "@types/react": "^18.0.0" + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@vitejs/plugin-react": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", - "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "node_modules/@oxlint/binding-win32-arm64-msvc": { + "version": "1.90.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.90.0.tgz", + "integrity": "sha512-KBvR3A9fO7fCi2vCP8ifWvlia7tGEQV9NaYh+IGwa/7Groy48Mt/UaYqMK9/zOtAWl20BUCf6RPqzJ32fRw33Q==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@babel/core": "^7.28.0", - "@babel/plugin-transform-react-jsx-self": "^7.27.1", - "@babel/plugin-transform-react-jsx-source": "^7.27.1", - "@rolldown/pluginutils": "1.0.0-beta.27", - "@types/babel__core": "^7.20.5", - "react-refresh": "^0.17.0" - }, + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "peerDependencies": { - "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@vitest/expect": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.9.tgz", - "integrity": "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==", + "node_modules/@oxlint/binding-win32-ia32-msvc": { + "version": "1.90.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.90.0.tgz", + "integrity": "sha512-+x42rDHQGbTCuT3EzpWAzhP3NoVPE/cJsG4gnrakOSg0olV+GCdMpPU23Nvrl3zZMnXxFZdTtDv4D3SuaA5ZeQ==", + "cpu": [ + "ia32" + ], "dev": true, "license": "MIT", - "dependencies": { - "@vitest/spy": "2.1.9", - "@vitest/utils": "2.1.9", - "chai": "^5.1.2", - "tinyrainbow": "^1.2.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@vitest/pretty-format": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.9.tgz", - "integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==", + "node_modules/@oxlint/binding-win32-x64-msvc": { + "version": "1.90.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.90.0.tgz", + "integrity": "sha512-30JiaFEBUB9peEOcvVM8ggo88SoN/IdSr9DW505JdBji/R6dM66gKEiG65hw6qcaF2f4mO0cEJ+RwRZprmun9g==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "tinyrainbow": "^1.2.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@vitest/runner": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.9.tgz", - "integrity": "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==", + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/utils": "2.1.9", - "pathe": "^1.1.2" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } + "license": "MIT" }, - "node_modules/@vitest/snapshot": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.9.tgz", - "integrity": "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==", + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.67.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.67.4.tgz", + "integrity": "sha512-/v/pUAPOcwp+r1Hjjdg0cBV1TE89CzGKrKh2FgkIQw2QBcIMmVPOCSfuOZStCn4Xh/98HXfo/ht6ztcssctPNg==", + "cpu": [ + "arm" + ], "dev": true, "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "2.1.9", - "magic-string": "^0.30.12", - "pathe": "^1.1.2" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } + "optional": true, + "os": [ + "android" + ] }, - "node_modules/@vitest/spy": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.9.tgz", - "integrity": "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==", + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.67.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.67.4.tgz", + "integrity": "sha512-6tmmB/P52Vt167FXXEw5auiEUgTtZz7aqUgWHqYXToIUE0HFDY6fTdAyeekjjsNk6O/8dtpSKtClxSj1mMneKQ==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "tinyspy": "^3.0.2" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } + "optional": true, + "os": [ + "android" + ] }, - "node_modules/@vitest/utils": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.9.tgz", - "integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==", + "node_modules/@rollup/rollup-android-x64": { + "version": "4.67.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-x64/-/rollup-android-x64-4.67.4.tgz", + "integrity": "sha512-8Z9Q4RmotVmuL4llOvqX814JNiOK1oqoBv+glG5KbpRfUiypqzz2/fxIzK4Pq129l4dT5a8Gszmpc0/XPYoFuQ==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "2.1.9", - "loupe": "^3.1.2", - "tinyrainbow": "^1.2.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/adler-32": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz", - "integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==", - "license": "Apache-2.0", - "engines": { - "node": ">=0.8" - } - }, - "node_modules/any-promise": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", - "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", - "dev": true, - "license": "MIT" + "optional": true, + "os": [ + "android" + ] }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.67.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.67.4.tgz", + "integrity": "sha512-pnq6hssxTtwuRAl6Kdi/+IDY+b1IhkUetjhUkdLAAJCssjfqtL2Oqy/FXJVJFtAWOu1oRlArIvf0Yk1O92x2HQ==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "ISC", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] }, - "node_modules/arg": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", - "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.67.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.67.4.tgz", + "integrity": "sha512-BeIrGZvHMVdJg/N+ohZfFywG/gF41684YhDj2wBvouj7m9ziP1tZDUNwzXRiDY+3WIKngaC4hiUBqVtBPGP5og==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] }, - "node_modules/assertion-error": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", - "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.67.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.67.4.tgz", + "integrity": "sha512-fRFX/oqNg/g780DKoS8RIfYcZD59Ru5O/bSeK1N7gQljBFrTxJY/q5EX4eMs5SrJf32VgVBVVYCdfPpzAwPPnQ==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "engines": { - "node": ">=12" - } - }, - "node_modules/autoprefixer": { - "version": "10.6.2", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.67.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.67.4.tgz", + "integrity": "sha512-bSrBRopG1q+0RuOsW0NtYTtIODlvNA3T3ebx5WnQVdSxWIoXFmj+rZdHUHdSLgFdZ5M6VGmekYE7r4RMGQXjfg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.67.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.67.4.tgz", + "integrity": "sha512-L8IvdrJVtl65g1ozwDSyjGPG+FmibhCdFUwmqTItJLw0S3+Oj4QzFs7rfgTUiK7rsPjkCLdG1/GApFxoynXFJQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.67.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.67.4.tgz", + "integrity": "sha512-h7PcoQOzC31jshNpQ48J0akB9tsghjkSjSl5AZ6XsVGTxVsln7t7C4SQyw62tNVSaz2SeHgk4VT2ELbL17BSHQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.67.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.67.4.tgz", + "integrity": "sha512-KSPuOBNHoOl7AsVjeY1KZci8+0up0U4TFAHU5IzBYN8P+Z6cB9DZq28+JQycZW7f6WLLa/tSzhV7Rdy95bVi+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.67.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.67.4.tgz", + "integrity": "sha512-0E/mKmVk1PlQeQYygIwQm0jlwjANTzGSiNBMPrFx+rdFGFj/2Axh4p6XwcrystQIqXZVudTaGsdbDpzYgJ4wSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.67.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.67.4.tgz", + "integrity": "sha512-Fvw7dOSc2Sa9UOipRZVI0zOgdLCzsjVyD/6JuHXzUHTXFu7mMgmeX/0/bCVBIut1dz+nWz3pV4WpS/Qg0XVnxw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.67.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.67.4.tgz", + "integrity": "sha512-jzxbVHaE7V3F+P0wEK7UydQtvv7Twfdtx58jI5/3VRx1jJxyrn+qFwfXmqjVFxPhBl+zjaRs+ZJMFJw7vdHaQA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.67.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.67.4.tgz", + "integrity": "sha512-fhtrzp9p4GjEJVaigaap9g6X9nP6xgJtKiLLXSDgpE18HXufA5aqfltGn+KAqMtVZReJZfOrXm//y5YSA9BWbA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.67.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.67.4.tgz", + "integrity": "sha512-uynaF7IUHwAckV0S+yC1f5esy9gB+aqCBsPH5BOck32TYdCuAn2lVG5tvUcFnEsrviGEmRZxE1NHSOieUPYJEg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.67.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.67.4.tgz", + "integrity": "sha512-uaqRFdfb8eIa2bVHN/FE3ndCvR0zV2xv0AwjCfxrqSu6l6fp/54VhjfhslKlf2RIik9L1jwlt4E56PeVX6tQtQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.67.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.67.4.tgz", + "integrity": "sha512-JZRow7kmxUDEASnxHuVG5itTm+nTXi9BQUABcVUnUEngC+OPwMrpx1uJ9hIA/v+IcAEBL52ktCSPXk5OJn9Jqg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.67.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.67.4.tgz", + "integrity": "sha512-rdrh4e+Lq9fCNKoDPaCqeHXbQCe7yl77Ukl45tlSClwYdZ1gpFGZz1XLJdYtY++FRlfL5aHvb6/0n3GDW/Nxsw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.67.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.67.4.tgz", + "integrity": "sha512-wL7MS4r0fYcxluF3ne2plofHZiB/LsqNCnyaUlyiKpAJLb6agPSFz7DEFCfkonWAZXMrLwCmC08b2ZEnEdJkog==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.67.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.67.4.tgz", + "integrity": "sha512-4H0XHfd2VufGjX7NjUyeue78sD0bRIoKSGjGqO+F5nSvL6jN48NAU7esdmtFEYxZXwObLtt3MJ/kHS0jASMW3g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.67.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.67.4.tgz", + "integrity": "sha512-osRHQbE55pZpCLetpMsFmhkapyZnAUH73qRKMh+npi3boUom6u6hdaTTPSxlInXxwJHsn9r2uDhdz0V7Yoxx2A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.67.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.67.4.tgz", + "integrity": "sha512-95+exYEyOwrZePsE0czi4q7X5lxKPa21+lmyAZlwbYENITQ0Unk4HAQAFp/p+KhXBHBx9mUABXRYjOC5A1wY3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.67.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.67.4.tgz", + "integrity": "sha512-FOXOcKhNQpO4Po+Ynan6IbtM67SrXZbgpRJWVYwz53CT5veI7sVzDwVBmWS65N5Imv+wK3SVhIPAl+C5DXXNCg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.67.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.67.4.tgz", + "integrity": "sha512-cklKh0gJOkAZQcWziQa3hrjxZKN+PCHZ7SPTyGJDAiBVr05ZXlEjGFCG7zFJMaVibxsmzAe1FimN4u/HBns9+Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.67.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.67.4.tgz", + "integrity": "sha512-AegwxJPc9bvUp8fQtlsUHkFMOMkGylWU7glyL+YD4vwG+QaYoGXYLSsQoeTD7kvoLZrVlMWNVhTg1dedY5C1VQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.67.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.67.4.tgz", + "integrity": "sha512-RJZnrwTWj7Iq2ooBwoGbDdC65tDwYawneGcAfFMJqVUrRZMcskka6nTkBpXR/47G7kzn3EW2qupExnaBy11vwg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@stylistic/eslint-plugin": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin/-/eslint-plugin-4.4.1.tgz", + "integrity": "sha512-CEigAk7eOLyHvdgmpZsKFwtiqS2wFwI1fn4j09IU9GmD4euFM4jEBAViWeCqaNLlbX2k2+A/Fq9cje4HQBXuJQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/utils": "^8.32.1", + "eslint-visitor-keys": "^4.2.0", + "espree": "^10.3.0", + "estraverse": "^5.3.0", + "picomatch": "^4.0.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "peerDependencies": { + "eslint": ">=9.0.0" + } + }, + "node_modules/@stylistic/eslint-plugin/node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@tanstack/history": { + "version": "1.162.5", + "resolved": "https://registry.npmjs.org/@tanstack/history/-/history-1.162.5.tgz", + "integrity": "sha512-w1+YvwmTkwt5n8yUqEH3IWZ9DsmorCZW/EQ3KYHnqEgIkOZpH54NvW6/Epq3Dzb8H0ifIeYX8PExvrNLIH5FIA==", + "license": "MIT", + "engines": { + "node": ">=20.19" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/query-core": { + "version": "5.105.1", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.105.1.tgz", + "integrity": "sha512-B/rNlZoZ54w1dlddiUt9oEe1SLM+37c00SKLJEg9YH+7as3Y54P81o9Fh+2ujZ36tXPnQJ5oVe7AcyEWkyFhPg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.105.1", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.105.1.tgz", + "integrity": "sha512-qcu+/+PtBOjW8qU4/0l47QVmNG7hPOWsCLzP0bTRhE1X7HYa757C4732wXTUH0QhCjTV1yedX8onsCiEwlNYrA==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.105.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "@types/react": "^18 || ^19", + "react": "^18 || ^19" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@tanstack/react-router": { + "version": "1.170.46", + "resolved": "https://registry.npmjs.org/@tanstack/react-router/-/react-router-1.170.46.tgz", + "integrity": "sha512-ez1iNIhsPjGlaDHg3BXWs67EWVIYzB2tbopEHYDESdkO7owgkGb5de52+5Kv2v1fxdkggcNzo35bdk4arTVGQw==", + "license": "MIT", + "dependencies": { + "@tanstack/history": "1.162.5", + "@tanstack/react-store": "^0.11.2", + "@tanstack/router-core": "1.171.39", + "isbot": "^5.1.22" + }, + "engines": { + "node": ">=20.19" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": ">=18.0.0 || >=19.0.0", + "react-dom": ">=18.0.0 || >=19.0.0" + } + }, + "node_modules/@tanstack/react-store": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/@tanstack/react-store/-/react-store-0.11.2.tgz", + "integrity": "sha512-oj5O5DmiMWE+ih+SioX2ayC1kbK7d3fpLx0B75fPJr9nY3zU+hoW010yr64qXsY/9fyiLVE2bn5mGNV3owm8RQ==", + "license": "MIT", + "dependencies": { + "@tanstack/store": "0.11.2", + "use-sync-external-store": "^1.6.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@tanstack/router-core": { + "version": "1.171.39", + "resolved": "https://registry.npmjs.org/@tanstack/router-core/-/router-core-1.171.39.tgz", + "integrity": "sha512-qUhoJ0meuSUqGYQbHbcJYdASr06YtK+bulbj6vtrhKYFkLgkSsZ6tXmMyym7sFxiSJSPYgasgozrL01LyyR/6A==", + "license": "MIT", + "dependencies": { + "@tanstack/history": "1.162.5", + "cookie-es": "^3.0.0", + "seroval": "^1.6.7", + "seroval-plugins": "^1.6.7" + }, + "engines": { + "node": ">=20.19" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/store": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/@tanstack/store/-/store-0.11.2.tgz", + "integrity": "sha512-sJ4mjol8uQsHV0gOJzzjwXfh2Fwm+Sz0+8deqiTm4jGbMdjzNSW+xZCFm0kUa870uhd8yi+DpKnZb5Kc4apa0Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-kVd74ta9eof3eJOvbNd1vGKS/XERRyQbT26Og63hIsvDO84cjD5gEOhsXf26w3FSoNlPVz84DOFcKv/oou+fMw==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@types/estree": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.10.tgz", + "integrity": "sha512-lTyOMnOcmRBzdCv6/4zUZCrEhV0xrqQiUZBZtVcbmLHBXKLnN8q89v/wKRF/Q0HbwVsqbIG3o5HDoFl9sduM+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@types/node": { + "version": "24.19.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.19.2.tgz", + "integrity": "sha512-93d49wCWJkCyN87MxLpNN6KXPCwFw7bVUg79mGUV1OS40JGWwVCr8n61DXCSqz7CV6/VfPkb7xec7uP2M+QHWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": ">=7.24.0 <7.24.7" + } + }, + "node_modules/@types/papaparse": { + "version": "5.5.2", + "resolved": "https://registry.npmjs.org/@types/papaparse/-/papaparse-5.5.2.tgz", + "integrity": "sha512-gFnFp/JMzLHCwRf7tQHrNnfhN4eYBVYYI897CGX4MY1tzY9l2aLkVyx2IlKZ/SAqDbB3I1AOZW5gTMGGsqWliA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/react": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.3.0.tgz", + "integrity": "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.3.0.tgz", + "integrity": "sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.3.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.72.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.72.1.tgz", + "integrity": "sha512-fAuVMkwdO/huI8nyo/WKnNKf6Gag93S56Rh3Ts5mGcavwSdaN8tRg1gCeIGX8PF+nzp1BjGfO4gHhGzW1J6jTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.72.1", + "@typescript-eslint/types": "^8.72.1", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.72.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.72.1.tgz", + "integrity": "sha512-lC/t6eMCZZQAAKZEYGL6RDVSuFehpdmMT6h+nJIjIa6oylN/PwDgDVtEIbB7O9xFfZYtZAYLoLlDNWKJhtF+uQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.72.1", + "@typescript-eslint/visitor-keys": "8.72.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.72.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.72.1.tgz", + "integrity": "sha512-e4D5R5vpeAEPBxT2BAzsFQL3sbwyeeeo5+aczgNSRK1V734jE7pTqaGFU+fXapWEQwnZbpFaTn32+Z8DwS6OTg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.72.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.72.1.tgz", + "integrity": "sha512-LmE5IhQet1BWOWEXqBrVtBPsMAOQ6Nzk35d+WLh3mk4EUF33sqlfxjio1MdYeSvKSG5skactKKuixBIKXhL4Rw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.72.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.72.1.tgz", + "integrity": "sha512-DAlUoUV93hIvrtIQLwCbmZQYVZwOMhiHJrOVUUIWicFZaIYjTRqlY3I/NZevfkLmSqkNHRu2qYDcq9lrn16oOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.72.1", + "@typescript-eslint/tsconfig-utils": "8.72.1", + "@typescript-eslint/types": "8.72.1", + "@typescript-eslint/visitor-keys": "8.72.1", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.72.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.72.1.tgz", + "integrity": "sha512-5v5liypurJBFHCBNqh/RjsymAD+VWcE0djuGY/BUqp5fC3Qn+luWDEksjuTtWjPn6i5zQlUtkZCRG/CYG0yCkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.72.1", + "@typescript-eslint/types": "8.72.1", + "@typescript-eslint/typescript-estree": "8.72.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.72.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.72.1.tgz", + "integrity": "sha512-SB8xqipSzZHUePCCLNeYxqnxq83X61/qzNhjQGbTfjxGZIeOEP7tBgWYX05Tcd6CgE02WdTJnh5b8kMZJ/+bnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.72.1", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/@vitest/expect": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.7.tgz", + "integrity": "sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.7", + "@vitest/utils": "3.2.7", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.7.tgz", + "integrity": "sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.7", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.7.tgz", + "integrity": "sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.7.tgz", + "integrity": "sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.7", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.7.tgz", + "integrity": "sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.7", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.7.tgz", + "integrity": "sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.7.tgz", + "integrity": "sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.7", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/acorn": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.19.0.tgz", + "integrity": "sha512-oJlA3XiRm7Cyk6qFD2Jn8ak9B7jSy0qy00ADO3+8dpT0LSjFihQYv4C02LFCSYJV3Q37xYwwRy5m+IpIiUzqWw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/adler-32": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz", + "integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, + "license": "MIT" + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/autoprefixer": { + "version": "10.6.2", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.6.2.tgz", "integrity": "sha512-VOw9/yGTGwPPgzFcODtcv6zhEPIeIWkRSe10L2ri2f5vZ7ID7lCt6jZzRF5/ubgOZ+szYydmEF5AYsA5jxX1tA==", "dev": true, @@ -1605,6 +2906,16 @@ "postcss": "^8.1.0" } }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, "node_modules/baseline-browser-mapping": { "version": "2.11.34", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.34.tgz", @@ -1631,6 +2942,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/brace-expansion": { + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, "node_modules/braces": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", @@ -1688,6 +3012,21 @@ "node": ">=8" } }, + "node_modules/cacheable": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/cacheable/-/cacheable-2.5.0.tgz", + "integrity": "sha512-60cyAOytib/OzBw1JNSoSV/boK1AtHryDIjvVBk7XbN4ugfkM3+Sry7fEjNgPMGgOjuaZPAp8ruZ0Cxafwyq9g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@cacheable/memory": "^2.2.0", + "@cacheable/utils": "^2.5.0", + "hookified": "^1.15.0", + "keyv": "^5.6.0", + "qified": "^0.10.1" + } + }, "node_modules/camelcase-css": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", @@ -1832,6 +3171,12 @@ "dev": true, "license": "MIT" }, + "node_modules/cookie-es": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-3.1.1.tgz", + "integrity": "sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg==", + "license": "MIT" + }, "node_modules/crc-32": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", @@ -1844,6 +3189,22 @@ "node": ">=0.8" } }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -1936,1449 +3297,1344 @@ "d3-time-format": "2 - 4" }, "engines": { - "node": ">=12" - } - }, - "node_modules/d3-shape": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", - "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", - "license": "ISC", - "dependencies": { - "d3-path": "^3.1.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-time": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", - "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", - "license": "ISC", - "dependencies": { - "d3-array": "2 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-time-format": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", - "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", - "license": "ISC", - "dependencies": { - "d3-time": "1 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-timer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", - "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/decimal.js-light": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", - "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", - "license": "MIT" - }, - "node_modules/deep-eql": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", - "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/didyoumean": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", - "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/dlv": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", - "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", - "dev": true, - "license": "MIT" - }, - "node_modules/dom-helpers": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", - "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.8.7", - "csstype": "^3.0.2" - } - }, - "node_modules/electron-to-chromium": { - "version": "1.5.472", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.472.tgz", - "integrity": "sha512-s4+nfT29b0FKpVKoaIxl2pWsoNcn7A/nMPSrxayw4T3BsmPeV2lF9Y06BsAQeMGhCTQ4nYJ5XST7jkqe4ZEqFw==", - "dev": true, - "license": "ISC" - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-module-lexer": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", - "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", - "dev": true, - "license": "MIT" - }, - "node_modules/esbuild": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", - "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.12", - "@esbuild/android-arm": "0.25.12", - "@esbuild/android-arm64": "0.25.12", - "@esbuild/android-x64": "0.25.12", - "@esbuild/darwin-arm64": "0.25.12", - "@esbuild/darwin-x64": "0.25.12", - "@esbuild/freebsd-arm64": "0.25.12", - "@esbuild/freebsd-x64": "0.25.12", - "@esbuild/linux-arm": "0.25.12", - "@esbuild/linux-arm64": "0.25.12", - "@esbuild/linux-ia32": "0.25.12", - "@esbuild/linux-loong64": "0.25.12", - "@esbuild/linux-mips64el": "0.25.12", - "@esbuild/linux-ppc64": "0.25.12", - "@esbuild/linux-riscv64": "0.25.12", - "@esbuild/linux-s390x": "0.25.12", - "@esbuild/linux-x64": "0.25.12", - "@esbuild/netbsd-arm64": "0.25.12", - "@esbuild/netbsd-x64": "0.25.12", - "@esbuild/openbsd-arm64": "0.25.12", - "@esbuild/openbsd-x64": "0.25.12", - "@esbuild/openharmony-arm64": "0.25.12", - "@esbuild/sunos-x64": "0.25.12", - "@esbuild/win32-arm64": "0.25.12", - "@esbuild/win32-ia32": "0.25.12", - "@esbuild/win32-x64": "0.25.12" - } - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/estree-walker": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0" - } - }, - "node_modules/eventemitter3": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", - "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", - "license": "MIT" - }, - "node_modules/expect-type": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", - "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/fast-equals": { - "version": "5.4.5", - "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.4.5.tgz", - "integrity": "sha512-yKEpcFuRPu/pTQL2VB8YKE2yFldrLlIsoWtJeBnAdqEhvu21OgkFT0C8njf/xYc2LSZGEr3K9JGjthHJxnVLvQ==", - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/fast-glob": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", - "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.8" - }, - "engines": { - "node": ">=8.6.0" + "node": ">=12" } }, - "node_modules/fast-glob/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", "license": "ISC", "dependencies": { - "is-glob": "^4.0.1" + "d3-path": "^3.1.0" }, "engines": { - "node": ">= 6" + "node": ">=12" } }, - "node_modules/fastq": { - "version": "1.20.3", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.3.tgz", - "integrity": "sha512-XKv5nnLs6nLF71NgiKJLIZFLkPyIEuOselLG7ujZnGrRfQK8HpvY+WqKhAJUAdLomwVHErVS4LfxFlPq0/FTAw==", - "dev": true, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", "license": "ISC", "dependencies": { - "reusify": "^1.0.4" + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" } }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "license": "MIT", + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", "dependencies": { - "to-regex-range": "^5.0.1" + "d3-time": "1 - 3" }, "engines": { - "node": ">=8" + "node": ">=12" } }, - "node_modules/frac": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz", - "integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==", - "license": "Apache-2.0", + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", "engines": { - "node": ">=0.8" + "node": ">=12" } }, - "node_modules/fraction.js": { - "version": "5.3.4", - "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", - "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, "engines": { - "node": "*" + "node": ">=6.0" }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/rawify" + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "node_modules/decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", + "license": "MIT" + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", "dev": true, - "hasInstallScript": true, "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + "node": ">=6" } }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true, "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } + "peer": true }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } + "license": "Apache-2.0" }, - "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } + "license": "MIT" }, - "node_modules/hasown": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", - "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", - "dev": true, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", "license": "MIT", "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/internmap": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", - "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", - "license": "ISC", - "engines": { - "node": ">=12" + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" } }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "node_modules/electron-to-chromium": { + "version": "1.5.472", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.472.tgz", + "integrity": "sha512-s4+nfT29b0FKpVKoaIxl2pWsoNcn7A/nMPSrxayw4T3BsmPeV2lF9Y06BsAQeMGhCTQ4nYJ5XST7jkqe4ZEqFw==", "dev": true, - "license": "MIT", - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } + "license": "ISC" }, - "node_modules/is-core-module": { - "version": "2.17.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.17.0.tgz", - "integrity": "sha512-J/vG0zBCbIKOQFfufSwyXdMrsohyJIUNkrnmo6WZGzoM7tr/lsbfW5b2BvisL6zsyMzK9UxV9L6c7AoFbyXHOA==", + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", "dev": true, "license": "MIT", - "dependencies": { - "hasown": "^2.0.4" - }, "engines": { "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } + "license": "MIT" }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", "dev": true, + "hasInstallScript": true, "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" + "bin": { + "esbuild": "bin/esbuild" }, "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" } }, - "node_modules/jiti": { - "version": "1.21.7", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", - "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true, "license": "MIT", - "bin": { - "jiti": "bin/jiti.js" + "engines": { + "node": ">=6" } }, - "node_modules/js-tokens": { + "node_modules/escape-string-regexp": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "license": "MIT" - }, - "node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "dev": true, "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, + "peer": true, "engines": { - "node": ">=6" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "node_modules/eslint": { + "version": "10.14.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.14.0.tgz", + "integrity": "sha512-T0pNsxkb9IKmtQjhpK3d7nWOpFv+YHuMbTJnSVMoQblF0onvXfErrtp4V1jHtsQPan7dKaGVD0q1WCXnzMF31A==", "dev": true, "license": "MIT", + "peer": true, + "workspaces": [ + "packages/*" + ], + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.5", + "@eslint/config-helpers": "^0.7.0", + "@eslint/core": "^1.2.1", + "@eslint/plugin-kit": "^0.7.3", + "@humanfs/node": "^0.16.8", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "11.1.5 || >11.1.6 <12", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "minimatch": "^10.2.6", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, "bin": { - "json5": "lib/cli.js" + "eslint": "bin/eslint.js" }, "engines": { - "node": ">=6" + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } } }, - "node_modules/lilconfig": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.4.tgz", - "integrity": "sha512-zJZyBmQhArDRefuWTypZxwDpFDIYmrj8ypSBvwGD5P13Y95s6DjzKgu4DVdGbeQa5PDEsAOCAPNk4X+QOR006w==", + "node_modules/eslint-scope": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", "dev": true, - "license": "MIT", + "license": "BSD-2-Clause", + "peer": true, + "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, "engines": { - "node": ">=14" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { - "url": "https://github.com/sponsors/antonk52" + "url": "https://opencollective.com/eslint" } }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", "dev": true, - "license": "MIT" - }, - "node_modules/lodash": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", - "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", - "license": "MIT" - }, - "node_modules/loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "license": "MIT", - "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, - "bin": { - "loose-envify": "cli.js" + "funding": { + "url": "https://opencollective.com/eslint" } }, - "node_modules/loupe": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", - "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", "dev": true, - "license": "MIT" + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } }, - "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "node_modules/eslint/node_modules/espree": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", "dev": true, - "license": "ISC", + "license": "BSD-2-Clause", + "peer": true, "dependencies": { - "yallist": "^3.0.2" - } - }, - "node_modules/lucide-react": { - "version": "0.577.0", - "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.577.0.tgz", - "integrity": "sha512-4LjoFv2eEPwYDPg/CUdBJQSDfPyzXCRrVW1X7jrx/trgxnxkHFjnVZINbzvzxjN70dxychOfg+FTYwBiS3pQ5A==", - "license": "ISC", - "peerDependencies": { - "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, - "node_modules/magic-string": { - "version": "0.30.21", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", - "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", "dev": true, - "license": "MIT", + "license": "BSD-2-Clause", "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.5" + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", "dev": true, - "license": "MIT", + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "estraverse": "^5.1.0" + }, "engines": { - "node": ">= 8" + "node": ">=0.10" } }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", "dev": true, - "license": "MIT", + "license": "BSD-2-Clause", + "peer": true, "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" + "estraverse": "^5.2.0" }, "engines": { - "node": ">=8.6" + "node": ">=4.0" } }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true, - "license": "MIT" + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } }, - "node_modules/mz": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", - "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", "dev": true, "license": "MIT", "dependencies": { - "any-promise": "^1.0.0", - "object-assign": "^4.0.1", - "thenify-all": "^1.0.0" + "@types/estree": "^1.0.0" } }, - "node_modules/nanoid": { - "version": "3.3.20", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.20.tgz", - "integrity": "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==", + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, + "license": "BSD-2-Clause", + "peer": true, "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + "node": ">=0.10.0" } }, - "node_modules/node-releases": { - "version": "2.0.63", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.63.tgz", - "integrity": "sha512-6li2QanLsAXpIH2R6j77MzRyvOyrvdTqlKtO4CgBdHGNvN99ZZfyxszHc5o5+IZBWD5u6Hzwg83JoGXmqZNG9Q==", + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "license": "MIT" + }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", "dev": true, - "license": "MIT", + "license": "Apache-2.0", "engines": { - "node": ">=18" + "node": ">=12.0.0" } }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true, "license": "MIT", - "engines": { - "node": ">=0.10.0" - } + "peer": true }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "node_modules/fast-equals": { + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.4.5.tgz", + "integrity": "sha512-yKEpcFuRPu/pTQL2VB8YKE2yFldrLlIsoWtJeBnAdqEhvu21OgkFT0C8njf/xYc2LSZGEr3K9JGjthHJxnVLvQ==", "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": ">=6.0.0" } }, - "node_modules/object-hash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", - "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", "dev": true, "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, "engines": { - "node": ">= 6" + "node": ">=8.6.0" } }, - "node_modules/papaparse": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.7.0.tgz", - "integrity": "sha512-qBGxg/7Q3Kl9Wfhrz2Z74UnvnHTXLNG6jmKJFeBvP2+y4lV7So+7SR62+Zd47JvdrCkX+nDcnr0ObPzek/+6RA==", - "license": "MIT" - }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true, - "license": "MIT" - }, - "node_modules/pathe": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", - "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/pathval": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", - "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "dev": true, - "license": "MIT", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, "engines": { - "node": ">= 14.16" + "node": ">= 6" } }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", "dev": true, - "license": "ISC" + "license": "MIT", + "peer": true }, - "node_modules/picomatch": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", - "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true, "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } + "peer": true }, - "node_modules/pirates": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", - "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "node_modules/fastq": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.3.tgz", + "integrity": "sha512-XKv5nnLs6nLF71NgiKJLIZFLkPyIEuOselLG7ujZnGrRfQK8HpvY+WqKhAJUAdLomwVHErVS4LfxFlPq0/FTAw==", "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" } }, - "node_modules/postcss": { - "version": "8.5.32", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.32.tgz", - "integrity": "sha512-LJlKCCAUFKOuZxipUdYw1NzIITEsPcgyOG9LkvRJvuKj/bRzbuRY0ARXvL0pWGpsZvn8jku+LMep2eU0seHp8g==", + "node_modules/file-entry-cache": { + "version": "11.1.5", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-11.1.5.tgz", + "integrity": "sha512-+PFTHITI08JIGhnNpGNI8T8inUpgZfk3GNEqfT9R2zZV2iFXg3CvqzSl/uEhs7TSGujYRELEANyDvS8Fj7+S7Q==", "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], "license": "MIT", + "peer": true, "dependencies": { - "nanoid": "^3.3.20", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.2" - }, - "engines": { - "node": "^10 || ^12 || >=14" + "flat-cache": "^6.1.23" } }, - "node_modules/postcss-import": { - "version": "15.1.0", - "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", - "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "license": "MIT", "dependencies": { - "postcss-value-parser": "^4.0.0", - "read-cache": "^1.0.0", - "resolve": "^1.1.7" + "to-regex-range": "^5.0.1" }, "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "postcss": "^8.0.0" + "node": ">=8" } }, - "node_modules/postcss-js": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", - "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], "license": "MIT", + "peer": true, "dependencies": { - "camelcase-css": "^2.0.1" + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" }, "engines": { - "node": "^12 || ^14 || >= 16" + "node": ">=10" }, - "peerDependencies": { - "postcss": "^8.4.21" + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/postcss-load-config": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", - "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "node_modules/flat-cache": { + "version": "6.1.23", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-6.1.23.tgz", + "integrity": "sha512-f++BY9pTk+983xK1FLzlLpmM0i0z+jHmx3QESGkURMXujQZz1k5wzwX6hjnQ8goaD0B+sYnDK1yZ6MTyZfUaqA==", "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], "license": "MIT", + "peer": true, "dependencies": { - "lilconfig": "^3.1.1" - }, + "cacheable": "^2.5.0", + "flatted": "^3.4.2", + "hookified": "^1.15.0" + } + }, + "node_modules/flatted": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", + "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", + "dev": true, + "license": "ISC", + "peer": true + }, + "node_modules/frac": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz", + "integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==", + "license": "Apache-2.0", "engines": { - "node": ">= 18" - }, - "peerDependencies": { - "jiti": ">=1.21.0", - "postcss": ">=8.0.9", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "jiti": { - "optional": true - }, - "postcss": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } + "node": ">=0.8" } }, - "node_modules/postcss-nested": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", - "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], "license": "MIT", - "dependencies": { - "postcss-selector-parser": "^6.1.1" - }, "engines": { - "node": ">=12.0" + "node": "*" }, - "peerDependencies": { - "postcss": "^8.2.14" + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" } }, - "node_modules/postcss-selector-parser": { - "version": "6.1.5", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.5.tgz", - "integrity": "sha512-TEHFzb5KRDKaOgJ4TYE+0jyJtFbjLvkdVOu0fZ2b6MTWzoqZd4Le+61aEppWkvTlYePgOX4ATL1wsAIPMLTw6Q==", + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, + "hasInstallScript": true, "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=4" + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", "dev": true, - "license": "MIT" - }, - "node_modules/prop-types": { - "version": "15.8.1", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", - "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", "license": "MIT", - "dependencies": { - "loose-envify": "^1.4.0", - "object-assign": "^4.1.1", - "react-is": "^16.13.1" + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/prop-types/node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "license": "MIT" + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } }, - "node_modules/react": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", - "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "node_modules/hashery": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/hashery/-/hashery-1.5.1.tgz", + "integrity": "sha512-iZyKG96/JwPz1N55vj2Ie2vXbhu440zfUfJvSwEqEbeLluk7NnapfGqa7LH0mOsnDxTF85Mx8/dyR6HfqcbmbQ==", + "dev": true, "license": "MIT", + "peer": true, "dependencies": { - "loose-envify": "^1.1.0" + "hookified": "^1.15.0" }, "engines": { - "node": ">=0.10.0" + "node": ">=20" } }, - "node_modules/react-dom": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", - "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dev": true, "license": "MIT", "dependencies": { - "loose-envify": "^1.1.0", - "scheduler": "^0.23.2" + "function-bind": "^1.1.2" }, - "peerDependencies": { - "react": "^18.3.1" + "engines": { + "node": ">= 0.4" } }, - "node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "license": "MIT" + "node_modules/hookified": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/hookified/-/hookified-1.15.1.tgz", + "integrity": "sha512-MvG/clsADq1GPM2KGo2nyfaWVyn9naPiXrqIe4jYjXNZQt238kWyOGrsyc/DmRAQ+Re6yeo6yX/yoNCG5KAEVg==", + "dev": true, + "license": "MIT", + "peer": true }, - "node_modules/react-refresh": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", - "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "dev": true, "license": "MIT", + "peer": true, "engines": { - "node": ">=0.10.0" + "node": ">= 4" } }, - "node_modules/react-smooth": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz", - "integrity": "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==", + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, "license": "MIT", - "dependencies": { - "fast-equals": "^5.0.1", - "prop-types": "^15.8.1", - "react-transition-group": "^4.4.5" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + "peer": true, + "engines": { + "node": ">=0.8.19" } }, - "node_modules/react-transition-group": { - "version": "4.4.5", - "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", - "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", - "license": "BSD-3-Clause", - "dependencies": { - "@babel/runtime": "^7.5.5", - "dom-helpers": "^5.0.1", - "loose-envify": "^1.4.0", - "prop-types": "^15.6.2" - }, - "peerDependencies": { - "react": ">=16.6.0", - "react-dom": ">=16.6.0" + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" } }, - "node_modules/read-cache": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.2.tgz", - "integrity": "sha512-/peqiBB/n07gQGLsWaHho3WfvUyRscw0gYTsEFMhrIe/nWLkYaf5SbKYjGYqtRV3aPwykJgF2VEMo1ac4bnsGA==", - "dev": true, - "license": "MIT" - }, - "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", "dev": true, "license": "MIT", "dependencies": { - "picomatch": "^2.2.1" + "binary-extensions": "^2.0.0" }, "engines": { - "node": ">=8.10.0" + "node": ">=8" } }, - "node_modules/recharts": { - "version": "2.15.4", - "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.4.tgz", - "integrity": "sha512-UT/q6fwS3c1dHbXv2uFgYJ9BMFHu3fwnd7AYZaEQhXuYQ4hgsxLvsUXzGdKeZrW5xopzDCvuA2N41WJ88I7zIw==", - "deprecated": "1.x and 2.x branches are no longer active. Bump to Recharts v3 to receive latest features and bugfixes. See https://github.com/recharts/recharts/wiki/3.0-migration-guide", + "node_modules/is-core-module": { + "version": "2.17.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.17.0.tgz", + "integrity": "sha512-J/vG0zBCbIKOQFfufSwyXdMrsohyJIUNkrnmo6WZGzoM7tr/lsbfW5b2BvisL6zsyMzK9UxV9L6c7AoFbyXHOA==", + "dev": true, "license": "MIT", "dependencies": { - "clsx": "^2.0.0", - "eventemitter3": "^4.0.1", - "lodash": "^4.17.21", - "react-is": "^18.3.1", - "react-smooth": "^4.0.4", - "recharts-scale": "^0.4.4", - "tiny-invariant": "^1.3.1", - "victory-vendor": "^36.6.8" + "hasown": "^2.0.4" }, "engines": { - "node": ">=14" + "node": ">= 0.4" }, - "peerDependencies": { - "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/recharts-scale": { - "version": "0.4.5", - "resolved": "https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.5.tgz", - "integrity": "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==", + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, "license": "MIT", - "dependencies": { - "decimal.js-light": "^2.4.1" + "engines": { + "node": ">=0.10.0" } }, - "node_modules/resolve": { - "version": "1.22.13", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.13.tgz", - "integrity": "sha512-Dj/cW9zBV2aRj6XzCxuGIs3ESZKv02jixne3q6jFZYZ3/Tg09gHHviSMDL4kObh9pqSahdRHEnDoaP2gn8YLhA==", + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "dev": true, "license": "MIT", "dependencies": { - "es-errors": "^1.3.0", - "is-core-module": "^2.17.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" + "is-extglob": "^2.1.1" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=0.10.0" } }, - "node_modules/reusify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", - "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true, "license": "MIT", "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" + "node": ">=0.12.0" } }, - "node_modules/rollup": { - "version": "4.67.4", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.67.4.tgz", - "integrity": "sha512-Lsv3oKcz5jgOQJej/p7nfwbVEdAbDT+z6OPf/SCdI+nF+q2ENp0pTKT35ZwXAmH8g45mfrIiJeEyAqKYSYoL9Q==", + "node_modules/isbot": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/isbot/-/isbot-5.2.2.tgz", + "integrity": "sha512-iQcBXcd+Rv/pkubRyGh2utW2j1oPG5hZY6TUhVPpqK4G+o3IbxpJNx04hgksjc/N7GK5pEorUxDeg31cFgEk/w==", + "license": "Unlicense", + "engines": { + "node": ">=18" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC", + "peer": true + }, + "node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", "dev": true, "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.10" - }, "bin": { - "rollup": "dist/bin/rollup" + "jiti": "bin/jiti.js" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" }, "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" + "node": ">=6" + } + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" }, - "optionalDependencies": { - "@napi-rs/lzma-linux-x64-gnu": "1.5.1", - "@rollup/rollup-android-arm-eabi": "4.67.4", - "@rollup/rollup-android-arm64": "4.67.4", - "@rollup/rollup-android-x64": "4.67.4", - "@rollup/rollup-darwin-arm64": "4.67.4", - "@rollup/rollup-darwin-x64": "4.67.4", - "@rollup/rollup-freebsd-arm64": "4.67.4", - "@rollup/rollup-freebsd-x64": "4.67.4", - "@rollup/rollup-linux-arm-gnueabihf": "4.67.4", - "@rollup/rollup-linux-arm-musleabihf": "4.67.4", - "@rollup/rollup-linux-arm64-gnu": "4.67.4", - "@rollup/rollup-linux-arm64-musl": "4.67.4", - "@rollup/rollup-linux-loong64-gnu": "4.67.4", - "@rollup/rollup-linux-loong64-musl": "4.67.4", - "@rollup/rollup-linux-ppc64-gnu": "4.67.4", - "@rollup/rollup-linux-ppc64-musl": "4.67.4", - "@rollup/rollup-linux-riscv64-gnu": "4.67.4", - "@rollup/rollup-linux-riscv64-musl": "4.67.4", - "@rollup/rollup-linux-s390x-gnu": "4.67.4", - "@rollup/rollup-linux-x64-gnu": "4.67.4", - "@rollup/rollup-linux-x64-musl": "4.67.4", - "@rollup/rollup-openbsd-x64": "4.67.4", - "@rollup/rollup-openharmony-arm64": "4.67.4", - "@rollup/rollup-win32-arm64-msvc": "4.67.4", - "@rollup/rollup-win32-ia32-msvc": "4.67.4", - "@rollup/rollup-win32-x64-gnu": "4.67.4", - "@rollup/rollup-win32-x64-msvc": "4.67.4", - "fsevents": "~2.3.2" + "engines": { + "node": ">=6" } }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "node_modules/keyv": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz", + "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], "license": "MIT", + "peer": true, "dependencies": { - "queue-microtask": "^1.2.2" + "@keyv/serialize": "^1.1.1" } }, - "node_modules/scheduler": { - "version": "0.23.2", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", - "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, "license": "MIT", + "peer": true, "dependencies": { - "loose-envify": "^1.1.0" + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" } }, - "node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "node_modules/lilconfig": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.4.tgz", + "integrity": "sha512-zJZyBmQhArDRefuWTypZxwDpFDIYmrj8ypSBvwGD5P13Y95s6DjzKgu4DVdGbeQa5PDEsAOCAPNk4X+QOR006w==", "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" } }, - "node_modules/siginfo": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", - "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "dev": true, - "license": "ISC" + "license": "MIT" }, - "node_modules/source-map-js": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", - "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", "dev": true, - "license": "BSD-3-Clause", + "license": "MIT", + "peer": true, + "dependencies": { + "p-locate": "^5.0.0" + }, "engines": { - "node": ">=0.10.0" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/ssf": { - "version": "0.11.2", - "resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz", - "integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==", - "license": "Apache-2.0", + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", "dependencies": { - "frac": "~1.1.2" + "js-tokens": "^3.0.0 || ^4.0.0" }, - "engines": { - "node": ">=0.8" + "bin": { + "loose-envify": "cli.js" } }, - "node_modules/stackback": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", - "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", "dev": true, "license": "MIT" }, - "node_modules/std-env": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", - "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", "dev": true, - "license": "MIT" + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } }, - "node_modules/sucrase": { - "version": "3.35.1", - "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", - "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "node_modules/lucide-react": { + "version": "0.577.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.577.0.tgz", + "integrity": "sha512-4LjoFv2eEPwYDPg/CUdBJQSDfPyzXCRrVW1X7jrx/trgxnxkHFjnVZINbzvzxjN70dxychOfg+FTYwBiS3pQ5A==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/gen-mapping": "^0.3.2", - "commander": "^4.0.0", - "lines-and-columns": "^1.1.6", - "mz": "^2.7.0", - "pirates": "^4.0.1", - "tinyglobby": "^0.2.11", - "ts-interface-checker": "^0.1.9" - }, - "bin": { - "sucrase": "bin/sucrase", - "sucrase-node": "bin/sucrase-node" - }, + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", "engines": { - "node": ">=16 || 14 >=14.17" + "node": ">= 8" } }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, "engines": { - "node": ">= 0.4" + "node": ">=8.6" + } + }, + "node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/tailwindcss": { - "version": "3.4.19", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", - "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", "dev": true, "license": "MIT", "dependencies": { - "@alloc/quick-lru": "^5.2.0", - "arg": "^5.0.2", - "chokidar": "^3.6.0", - "didyoumean": "^1.2.2", - "dlv": "^1.1.3", - "fast-glob": "^3.3.2", - "glob-parent": "^6.0.2", - "is-glob": "^4.0.3", - "jiti": "^1.21.7", - "lilconfig": "^3.1.3", - "micromatch": "^4.0.8", - "normalize-path": "^3.0.0", - "object-hash": "^3.0.0", - "picocolors": "^1.1.1", - "postcss": "^8.4.47", - "postcss-import": "^15.1.0", - "postcss-js": "^4.0.1", - "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", - "postcss-nested": "^6.2.0", - "postcss-selector-parser": "^6.1.2", - "resolve": "^1.22.8", - "sucrase": "^3.35.0" - }, + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.20", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.20.tgz", + "integrity": "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", "bin": { - "tailwind": "lib/cli.js", - "tailwindcss": "lib/cli.js" + "nanoid": "bin/nanoid.cjs" }, "engines": { - "node": ">=14.0.0" + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, - "node_modules/thenify": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", - "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true, "license": "MIT", - "dependencies": { - "any-promise": "^1.0.0" + "peer": true + }, + "node_modules/node-releases": { + "version": "2.0.63", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.63.tgz", + "integrity": "sha512-6li2QanLsAXpIH2R6j77MzRyvOyrvdTqlKtO4CgBdHGNvN99ZZfyxszHc5o5+IZBWD5u6Hzwg83JoGXmqZNG9Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" } }, - "node_modules/thenify-all": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", - "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", "dev": true, "license": "MIT", - "dependencies": { - "thenify": ">= 3.1.0 < 4" - }, "engines": { - "node": ">=0.8" + "node": ">=0.10.0" } }, - "node_modules/tiny-invariant": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", - "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", - "license": "MIT" + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } }, - "node_modules/tinybench": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", - "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">= 6" + } }, - "node_modules/tinyexec": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", - "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true, + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } }, - "node_modules/tinyglobby": { - "version": "0.2.17", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", - "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "node_modules/oxfmt": { + "version": "0.75.0", + "resolved": "https://registry.npmjs.org/oxfmt/-/oxfmt-0.75.0.tgz", + "integrity": "sha512-/ZKvY5asfsVi9hdkOmRIumqOPu6kxlTgiJSZf54uq29kKew+Gu1FFAnEkGePyvLVW+MAYu41D6XggnzoElQzPw==", "dev": true, "license": "MIT", "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.4" + "tinypool": "2.2.0" + }, + "bin": { + "oxfmt": "bin/oxfmt" }, "engines": { - "node": ">=12.0.0" + "node": "^20.19.0 || >=22.12.0" }, "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" + "url": "https://github.com/sponsors/oxc-project" + }, + "optionalDependencies": { + "@oxfmt/binding-android-arm-eabi": "0.75.0", + "@oxfmt/binding-android-arm64": "0.75.0", + "@oxfmt/binding-darwin-arm64": "0.75.0", + "@oxfmt/binding-darwin-x64": "0.75.0", + "@oxfmt/binding-freebsd-x64": "0.75.0", + "@oxfmt/binding-linux-arm-gnueabihf": "0.75.0", + "@oxfmt/binding-linux-arm-musleabihf": "0.75.0", + "@oxfmt/binding-linux-arm64-gnu": "0.75.0", + "@oxfmt/binding-linux-arm64-musl": "0.75.0", + "@oxfmt/binding-linux-ppc64-gnu": "0.75.0", + "@oxfmt/binding-linux-riscv64-gnu": "0.75.0", + "@oxfmt/binding-linux-riscv64-musl": "0.75.0", + "@oxfmt/binding-linux-s390x-gnu": "0.75.0", + "@oxfmt/binding-linux-x64-gnu": "0.75.0", + "@oxfmt/binding-linux-x64-musl": "0.75.0", + "@oxfmt/binding-openharmony-arm64": "0.75.0", + "@oxfmt/binding-win32-arm64-msvc": "0.75.0", + "@oxfmt/binding-win32-ia32-msvc": "0.75.0", + "@oxfmt/binding-win32-x64-msvc": "0.75.0" + }, + "peerDependencies": { + "@astrojs/compiler-rs": "^0.5.0", + "svelte": "^5.0.0", + "vite-plus": "*" + }, + "peerDependenciesMeta": { + "@astrojs/compiler-rs": { + "optional": true + }, + "svelte": { + "optional": true + }, + "vite-plus": { + "optional": true + } } }, - "node_modules/tinyglobby/node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "node_modules/oxfmt/node_modules/tinypool": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-2.2.0.tgz", + "integrity": "sha512-jBrmx4lYmaC9k/mgPbylxs7kBUxHtD8256up+HjLaDFfXScKJQyil+SWXSvhAtT7XHo+yTVlpB81PGmHP8oLSQ==", "dev": true, "license": "MIT", "engines": { - "node": ">=12.0.0" + "node": "^20.0.0 || >=22.0.0" + } + }, + "node_modules/oxlint": { + "version": "1.90.0", + "resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.90.0.tgz", + "integrity": "sha512-RkTN9EmgIUpLuhNoLhO3OliMGJlvb3Dk7XVjBZnfoMCHrVpuz5ESd97klfiyYeV7XfA2lNLnDCPMkUI+Qw5How==", + "dev": true, + "license": "MIT", + "bin": { + "oxlint": "bin/oxlint" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/sponsors/oxc-project" + }, + "optionalDependencies": { + "@oxlint/binding-android-arm-eabi": "1.90.0", + "@oxlint/binding-android-arm64": "1.90.0", + "@oxlint/binding-darwin-arm64": "1.90.0", + "@oxlint/binding-darwin-x64": "1.90.0", + "@oxlint/binding-freebsd-x64": "1.90.0", + "@oxlint/binding-linux-arm-gnueabihf": "1.90.0", + "@oxlint/binding-linux-arm-musleabihf": "1.90.0", + "@oxlint/binding-linux-arm64-gnu": "1.90.0", + "@oxlint/binding-linux-arm64-musl": "1.90.0", + "@oxlint/binding-linux-ppc64-gnu": "1.90.0", + "@oxlint/binding-linux-riscv64-gnu": "1.90.0", + "@oxlint/binding-linux-riscv64-musl": "1.90.0", + "@oxlint/binding-linux-s390x-gnu": "1.90.0", + "@oxlint/binding-linux-x64-gnu": "1.90.0", + "@oxlint/binding-linux-x64-musl": "1.90.0", + "@oxlint/binding-openharmony-arm64": "1.90.0", + "@oxlint/binding-win32-arm64-msvc": "1.90.0", + "@oxlint/binding-win32-ia32-msvc": "1.90.0", + "@oxlint/binding-win32-x64-msvc": "1.90.0" }, "peerDependencies": { - "picomatch": "^3 || ^4" + "oxlint-tsgolint": ">=7.0.2005", + "vite-plus": "*" }, "peerDependenciesMeta": { - "picomatch": { + "oxlint-tsgolint": { + "optional": true + }, + "vite-plus": { "optional": true } } }, - "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", - "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", "dev": true, "license": "MIT", + "peer": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, "engines": { - "node": ">=12" + "node": ">=10" }, "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/tinypool": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", - "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", "dev": true, "license": "MIT", + "peer": true, + "dependencies": { + "p-limit": "^3.0.2" + }, "engines": { - "node": "^18.0.0 || >=20.0.0" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/tinyrainbow": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", - "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", + "node_modules/papaparse": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.7.0.tgz", + "integrity": "sha512-qBGxg/7Q3Kl9Wfhrz2Z74UnvnHTXLNG6jmKJFeBvP2+y4lV7So+7SR62+Zd47JvdrCkX+nDcnr0ObPzek/+6RA==", + "license": "MIT" + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", "dev": true, "license": "MIT", + "peer": true, "engines": { - "node": ">=14.0.0" + "node": ">=8" } }, - "node_modules/tinyspy": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", - "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { - "node": ">=14.0.0" + "node": ">=8" } }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", "dev": true, "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, "engines": { - "node": ">=8.0" + "node": ">= 14.16" } }, - "node_modules/ts-interface-checker": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", - "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "dev": true, - "license": "Apache-2.0" + "license": "ISC" }, - "node_modules/typescript": { - "version": "5.6.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", - "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, + "license": "MIT", "engines": { - "node": ">=14.17" + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">= 6" + } }, - "node_modules/update-browserslist-db": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.4.tgz", - "integrity": "sha512-t8wonIOdQyOjtPd2VktlBu6QjTVTks6v8bSi7kd0r2Vll8TK1JYDZtRRdbmCHOAMphkZgrzK+L+vXK4pyZ8t4A==", + "node_modules/postcss": { + "version": "8.5.32", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.32.tgz", + "integrity": "sha512-LJlKCCAUFKOuZxipUdYw1NzIITEsPcgyOG9LkvRJvuKj/bRzbuRY0ARXvL0pWGpsZvn8jku+LMep2eU0seHp8g==", "dev": true, "funding": [ { "type": "opencollective", - "url": "https://opencollective.com/browserslist" + "url": "https://opencollective.com/postcss/" }, { "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" + "url": "https://tidelift.com/funding/github/npm/postcss" }, { "type": "github", @@ -3387,110 +4643,91 @@ ], "license": "MIT", "dependencies": { - "escalade": "^3.2.0", - "picocolors": "^1.1.1" + "nanoid": "^3.3.20", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.2" }, - "bin": { - "update-browserslist-db": "cli.js" + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" }, "peerDependencies": { - "browserslist": ">= 4.21.0" + "postcss": "^8.0.0" } }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "node_modules/postcss-js": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", + "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", "dev": true, - "license": "MIT" - }, - "node_modules/victory-vendor": { - "version": "36.9.2", - "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz", - "integrity": "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==", - "license": "MIT AND ISC", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", "dependencies": { - "@types/d3-array": "^3.0.3", - "@types/d3-ease": "^3.0.0", - "@types/d3-interpolate": "^3.0.1", - "@types/d3-scale": "^4.0.2", - "@types/d3-shape": "^3.1.0", - "@types/d3-time": "^3.0.0", - "@types/d3-timer": "^3.0.0", - "d3-array": "^3.1.6", - "d3-ease": "^3.0.1", - "d3-interpolate": "^3.0.1", - "d3-scale": "^4.0.2", - "d3-shape": "^3.1.0", - "d3-time": "^3.0.0", - "d3-timer": "^3.0.1" + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "peerDependencies": { + "postcss": "^8.4.21" } }, - "node_modules/vite": { - "version": "6.4.5", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.5.tgz", - "integrity": "sha512-4enwjFfi4IwtEh0w+jPzpLVOFQiPHmmzPxcHXQOyEur47o6u0jE573DmGpjA+7irXoMLYRZD5ZaFUEneS3abPg==", + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "license": "MIT", "dependencies": { - "esbuild": "^0.25.0", - "fdir": "^6.4.4", - "picomatch": "^4.0.2", - "postcss": "^8.5.3", - "rollup": "^4.34.9", - "tinyglobby": "^0.2.13" - }, - "bin": { - "vite": "bin/vite.js" + "lilconfig": "^3.1.1" }, "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" + "node": ">= 18" }, "peerDependencies": { - "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.16.0", + "postcss": ">=8.0.9", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, "jiti": { "optional": true }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { + "postcss": { "optional": true }, "tsx": { @@ -3501,1089 +4738,847 @@ } } }, - "node_modules/vite-node": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.9.tgz", - "integrity": "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==", + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "license": "MIT", "dependencies": { - "cac": "^6.7.14", - "debug": "^4.3.7", - "es-module-lexer": "^1.5.4", - "pathe": "^1.1.2", - "vite": "^5.0.0" - }, - "bin": { - "vite-node": "vite-node.mjs" + "postcss-selector-parser": "^6.1.1" }, "engines": { - "node": "^18.0.0 || >=20.0.0" + "node": ">=12.0" }, - "funding": { - "url": "https://opencollective.com/vitest" + "peerDependencies": { + "postcss": "^8.2.14" } }, - "node_modules/vite-node/node_modules/@esbuild/aix-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", - "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", - "cpu": [ - "ppc64" - ], + "node_modules/postcss-selector-parser": { + "version": "6.1.5", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.5.tgz", + "integrity": "sha512-TEHFzb5KRDKaOgJ4TYE+0jyJtFbjLvkdVOu0fZ2b6MTWzoqZd4Le+61aEppWkvTlYePgOX4ATL1wsAIPMLTw6Q==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "aix" - ], + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, "engines": { - "node": ">=12" + "node": ">=4" } }, - "node_modules/vite-node/node_modules/@esbuild/android-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", - "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", - "cpu": [ - "arm" - ], + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } + "license": "MIT" }, - "node_modules/vite-node/node_modules/@esbuild/android-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", - "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", - "cpu": [ - "arm64" - ], + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "android" - ], + "peer": true, "engines": { - "node": ">=12" + "node": ">= 0.8.0" } }, - "node_modules/vite-node/node_modules/@esbuild/android-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", - "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", - "cpu": [ - "x64" - ], - "dev": true, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" } }, - "node_modules/vite-node/node_modules/@esbuild/darwin-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", - "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" }, - "node_modules/vite-node/node_modules/@esbuild/darwin-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", - "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", - "cpu": [ - "x64" - ], + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], + "peer": true, "engines": { - "node": ">=12" + "node": ">=6" } }, - "node_modules/vite-node/node_modules/@esbuild/freebsd-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", - "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", - "cpu": [ - "arm64" - ], + "node_modules/qified": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/qified/-/qified-0.10.1.tgz", + "integrity": "sha512-+Owyggi9IxT1ePKGafcI87ubSmxol6smwJ+RAHDQlx9+9cPwFWDiKFFCPuWhr9ignlGpZ9vDQLw67N4dcTVFEA==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], + "peer": true, + "dependencies": { + "hookified": "^2.1.1" + }, "engines": { - "node": ">=12" + "node": ">=20" } }, - "node_modules/vite-node/node_modules/@esbuild/freebsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", - "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", - "cpu": [ - "x64" - ], + "node_modules/qified/node_modules/hookified": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/hookified/-/hookified-2.2.0.tgz", + "integrity": "sha512-p/LgFzRN5FeoD3DLS6bkUapeye6E4SI6yJs6KetENd18S+FBthqYq2amJUWpt5z0EQwwHemidjY5OqJGEKm5uA==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } + "peer": true }, - "node_modules/vite-node/node_modules/@esbuild/linux-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", - "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", - "cpu": [ - "arm" - ], + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } ], - "engines": { - "node": ">=12" - } + "license": "MIT" }, - "node_modules/vite-node/node_modules/@esbuild/linux-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", - "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", - "cpu": [ - "arm64" - ], - "dev": true, + "node_modules/react": { + "version": "19.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-19.3.1.tgz", + "integrity": "sha512-QNhWdt7nrgVWWQG58QuGsAR3jv3Hbe4mb4vz7w2csnHavciZJgda1CPNaldF6eVxv90ud3jUXpKlHL69gO38yw==", "license": "MIT", - "optional": true, - "os": [ - "linux" - ], "engines": { - "node": ">=12" + "node": ">=0.10.0" } }, - "node_modules/vite-node/node_modules/@esbuild/linux-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", - "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", - "cpu": [ - "ia32" - ], - "dev": true, + "node_modules/react-dom": { + "version": "19.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.3.1.tgz", + "integrity": "sha512-Y0bARz53lhEC1NMCASs0/j+faB/zqt3R8hpvg1e8+61ZVJ4UL0UdC4cBbXfZMcIuhrfgeILTLe+ZeBCavQPtKQ==", "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" + "dependencies": { + "scheduler": "^0.28.0" + }, + "peerDependencies": { + "react": "^19.3.1" } }, - "node_modules/vite-node/node_modules/@esbuild/linux-loong64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", - "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "license": "MIT" }, - "node_modules/vite-node/node_modules/@esbuild/linux-mips64el": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", - "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", - "cpu": [ - "mips64el" - ], + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], "engines": { - "node": ">=12" + "node": ">=0.10.0" } }, - "node_modules/vite-node/node_modules/@esbuild/linux-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", - "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", - "cpu": [ - "ppc64" - ], - "dev": true, + "node_modules/react-smooth": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz", + "integrity": "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==", "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" + "dependencies": { + "fast-equals": "^5.0.1", + "prop-types": "^15.8.1", + "react-transition-group": "^4.4.5" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, - "node_modules/vite-node/node_modules/@esbuild/linux-riscv64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", - "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", - "cpu": [ - "riscv64" - ], + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "license": "BSD-3-Clause", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, + "node_modules/read-cache": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.2.tgz", + "integrity": "sha512-/peqiBB/n07gQGLsWaHho3WfvUyRscw0gYTsEFMhrIe/nWLkYaf5SbKYjGYqtRV3aPwykJgF2VEMo1ac4bnsGA==", + "dev": true, + "license": "MIT" + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "picomatch": "^2.2.1" + }, "engines": { - "node": ">=12" + "node": ">=8.10.0" } }, - "node_modules/vite-node/node_modules/@esbuild/linux-s390x": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", - "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", - "cpu": [ - "s390x" - ], - "dev": true, + "node_modules/recharts": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.4.tgz", + "integrity": "sha512-UT/q6fwS3c1dHbXv2uFgYJ9BMFHu3fwnd7AYZaEQhXuYQ4hgsxLvsUXzGdKeZrW5xopzDCvuA2N41WJ88I7zIw==", + "deprecated": "1.x and 2.x branches are no longer active. Bump to Recharts v3 to receive latest features and bugfixes. See https://github.com/recharts/recharts/wiki/3.0-migration-guide", "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "clsx": "^2.0.0", + "eventemitter3": "^4.0.1", + "lodash": "^4.17.21", + "react-is": "^18.3.1", + "react-smooth": "^4.0.4", + "recharts-scale": "^0.4.4", + "tiny-invariant": "^1.3.1", + "victory-vendor": "^36.6.8" + }, "engines": { - "node": ">=12" + "node": ">=14" + }, + "peerDependencies": { + "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, - "node_modules/vite-node/node_modules/@esbuild/linux-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", - "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", - "cpu": [ - "x64" - ], - "dev": true, + "node_modules/recharts-scale": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.5.tgz", + "integrity": "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==", "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" + "dependencies": { + "decimal.js-light": "^2.4.1" } }, - "node_modules/vite-node/node_modules/@esbuild/netbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", - "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", - "cpu": [ - "x64" - ], + "node_modules/resolve": { + "version": "1.22.13", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.13.tgz", + "integrity": "sha512-Dj/cW9zBV2aRj6XzCxuGIs3ESZKv02jixne3q6jFZYZ3/Tg09gHHviSMDL4kObh9pqSahdRHEnDoaP2gn8YLhA==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.17.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, "engines": { - "node": ">=12" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/vite-node/node_modules/@esbuild/openbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", - "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", - "cpu": [ - "x64" - ], + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], "engines": { - "node": ">=12" + "iojs": ">=1.0.0", + "node": ">=0.10.0" } }, - "node_modules/vite-node/node_modules/@esbuild/sunos-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", - "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", - "cpu": [ - "x64" - ], + "node_modules/rollup": { + "version": "4.67.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.67.4.tgz", + "integrity": "sha512-Lsv3oKcz5jgOQJej/p7nfwbVEdAbDT+z6OPf/SCdI+nF+q2ENp0pTKT35ZwXAmH8g45mfrIiJeEyAqKYSYoL9Q==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], + "dependencies": { + "@types/estree": "^1.0.10" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, "engines": { - "node": ">=12" + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@napi-rs/lzma-linux-x64-gnu": "1.5.1", + "@rollup/rollup-android-arm-eabi": "4.67.4", + "@rollup/rollup-android-arm64": "4.67.4", + "@rollup/rollup-android-x64": "4.67.4", + "@rollup/rollup-darwin-arm64": "4.67.4", + "@rollup/rollup-darwin-x64": "4.67.4", + "@rollup/rollup-freebsd-arm64": "4.67.4", + "@rollup/rollup-freebsd-x64": "4.67.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.67.4", + "@rollup/rollup-linux-arm-musleabihf": "4.67.4", + "@rollup/rollup-linux-arm64-gnu": "4.67.4", + "@rollup/rollup-linux-arm64-musl": "4.67.4", + "@rollup/rollup-linux-loong64-gnu": "4.67.4", + "@rollup/rollup-linux-loong64-musl": "4.67.4", + "@rollup/rollup-linux-ppc64-gnu": "4.67.4", + "@rollup/rollup-linux-ppc64-musl": "4.67.4", + "@rollup/rollup-linux-riscv64-gnu": "4.67.4", + "@rollup/rollup-linux-riscv64-musl": "4.67.4", + "@rollup/rollup-linux-s390x-gnu": "4.67.4", + "@rollup/rollup-linux-x64-gnu": "4.67.4", + "@rollup/rollup-linux-x64-musl": "4.67.4", + "@rollup/rollup-openbsd-x64": "4.67.4", + "@rollup/rollup-openharmony-arm64": "4.67.4", + "@rollup/rollup-win32-arm64-msvc": "4.67.4", + "@rollup/rollup-win32-ia32-msvc": "4.67.4", + "@rollup/rollup-win32-x64-gnu": "4.67.4", + "@rollup/rollup-win32-x64-msvc": "4.67.4", + "fsevents": "~2.3.2" } }, - "node_modules/vite-node/node_modules/@esbuild/win32-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", - "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", - "cpu": [ - "arm64" - ], + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } ], - "engines": { - "node": ">=12" + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" } }, - "node_modules/vite-node/node_modules/@esbuild/win32-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", - "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", - "cpu": [ - "ia32" - ], + "node_modules/scheduler": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.28.0.tgz", + "integrity": "sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" + "license": "ISC", + "bin": { + "semver": "bin/semver.js" } }, - "node_modules/vite-node/node_modules/@esbuild/win32-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", - "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", - "cpu": [ - "x64" - ], - "dev": true, + "node_modules/seroval": { + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/seroval/-/seroval-1.7.6.tgz", + "integrity": "sha512-oKNe6A8I3lSA5J0xg9xsrWCWDpET6CFhCMVuIsCmqrjNpBXzQXDAAQI5UfuC4Ner4DIR+VAtSw7TC0AmRMqFFQ==", "license": "MIT", - "optional": true, - "os": [ - "win32" - ], "engines": { - "node": ">=12" + "node": ">=20" } }, - "node_modules/vite-node/node_modules/esbuild": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", - "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", - "dev": true, - "hasInstallScript": true, + "node_modules/seroval-plugins": { + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/seroval-plugins/-/seroval-plugins-1.7.6.tgz", + "integrity": "sha512-/TPCAd6q5stAZnugD2fsvNKtM2UpgwrS8/fO1cXTgPC0EqCsGB+UfkEEsToFCfg35CDTMlWkppfr/zWnLJGpBA==", "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.21.5", - "@esbuild/android-arm": "0.21.5", - "@esbuild/android-arm64": "0.21.5", - "@esbuild/android-x64": "0.21.5", - "@esbuild/darwin-arm64": "0.21.5", - "@esbuild/darwin-x64": "0.21.5", - "@esbuild/freebsd-arm64": "0.21.5", - "@esbuild/freebsd-x64": "0.21.5", - "@esbuild/linux-arm": "0.21.5", - "@esbuild/linux-arm64": "0.21.5", - "@esbuild/linux-ia32": "0.21.5", - "@esbuild/linux-loong64": "0.21.5", - "@esbuild/linux-mips64el": "0.21.5", - "@esbuild/linux-ppc64": "0.21.5", - "@esbuild/linux-riscv64": "0.21.5", - "@esbuild/linux-s390x": "0.21.5", - "@esbuild/linux-x64": "0.21.5", - "@esbuild/netbsd-x64": "0.21.5", - "@esbuild/openbsd-x64": "0.21.5", - "@esbuild/sunos-x64": "0.21.5", - "@esbuild/win32-arm64": "0.21.5", - "@esbuild/win32-ia32": "0.21.5", - "@esbuild/win32-x64": "0.21.5" - } - }, - "node_modules/vite-node/node_modules/vite": { - "version": "5.4.21", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", - "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", - "dev": true, - "license": "MIT", - "dependencies": { - "esbuild": "^0.21.3", - "postcss": "^8.4.43", - "rollup": "^4.20.0" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^18.0.0 || >=20.0.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" + "node": ">=20" }, "peerDependencies": { - "@types/node": "^18.0.0 || >=20.0.0", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.4.0" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - } + "seroval": "^1.0" } }, - "node_modules/vite/node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "dev": true, "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" + "peer": true, + "dependencies": { + "shebang-regex": "^3.0.0" }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } + "engines": { + "node": ">=8" } }, - "node_modules/vite/node_modules/picomatch": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", - "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true, "license": "MIT", + "peer": true, "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "node": ">=8" } }, - "node_modules/vitest": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.9.tgz", - "integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/expect": "2.1.9", - "@vitest/mocker": "2.1.9", - "@vitest/pretty-format": "^2.1.9", - "@vitest/runner": "2.1.9", - "@vitest/snapshot": "2.1.9", - "@vitest/spy": "2.1.9", - "@vitest/utils": "2.1.9", - "chai": "^5.1.2", - "debug": "^4.3.7", - "expect-type": "^1.1.0", - "magic-string": "^0.30.12", - "pathe": "^1.1.2", - "std-env": "^3.8.0", - "tinybench": "^2.9.0", - "tinyexec": "^0.3.1", - "tinypool": "^1.0.1", - "tinyrainbow": "^1.2.0", - "vite": "^5.0.0", - "vite-node": "2.1.9", - "why-is-node-running": "^2.3.0" - }, - "bin": { - "vitest": "vitest.mjs" - }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map-js": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", + "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", + "dev": true, + "license": "BSD-3-Clause", "engines": { - "node": "^18.0.0 || >=20.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "@edge-runtime/vm": "*", - "@types/node": "^18.0.0 || >=20.0.0", - "@vitest/browser": "2.1.9", - "@vitest/ui": "2.1.9", - "happy-dom": "*", - "jsdom": "*" + "node": ">=0.10.0" + } + }, + "node_modules/ssf": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz", + "integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==", + "license": "Apache-2.0", + "dependencies": { + "frac": "~1.1.2" }, - "peerDependenciesMeta": { - "@edge-runtime/vm": { - "optional": true - }, - "@types/node": { - "optional": true - }, - "@vitest/browser": { - "optional": true - }, - "@vitest/ui": { - "optional": true - }, - "happy-dom": { - "optional": true - }, - "jsdom": { - "optional": true - } + "engines": { + "node": ">=0.8" } }, - "node_modules/vitest/node_modules/@esbuild/aix-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", - "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", - "cpu": [ - "ppc64" - ], + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/strip-literal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=12" + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" } }, - "node_modules/vitest/node_modules/@esbuild/android-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", - "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", - "cpu": [ - "arm" - ], + "node_modules/strip-literal/node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "android" - ], + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, "engines": { - "node": ">=12" + "node": ">=16 || 14 >=14.17" } }, - "node_modules/vitest/node_modules/@esbuild/android-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", - "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", - "cpu": [ - "arm64" - ], + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "android" - ], "engines": { - "node": ">=12" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/vitest/node_modules/@esbuild/android-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", - "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", - "cpu": [ - "x64" - ], + "node_modules/tailwindcss": { + "version": "3.4.19", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", + "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "android" - ], + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.7", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, "engines": { - "node": ">=12" + "node": ">=14.0.0" } }, - "node_modules/vitest/node_modules/@esbuild/darwin-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", - "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", - "cpu": [ - "arm64" - ], + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" + "dependencies": { + "any-promise": "^1.0.0" } }, - "node_modules/vitest/node_modules/@esbuild/darwin-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", - "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", - "cpu": [ - "x64" - ], + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, "engines": { - "node": ">=12" + "node": ">=0.8" } }, - "node_modules/vitest/node_modules/@esbuild/freebsd-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", - "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", - "cpu": [ - "arm64" - ], + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } + "license": "MIT" }, - "node_modules/vitest/node_modules/@esbuild/freebsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", - "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", - "cpu": [ - "x64" - ], + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } + "license": "MIT" }, - "node_modules/vitest/node_modules/@esbuild/linux-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", - "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", - "cpu": [ - "arm" - ], + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, "engines": { - "node": ">=12" + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" } }, - "node_modules/vitest/node_modules/@esbuild/linux-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", - "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", - "cpu": [ - "arm64" - ], + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], "engines": { - "node": ">=12" + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } } }, - "node_modules/vitest/node_modules/@esbuild/linux-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", - "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", - "cpu": [ - "ia32" - ], + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], "engines": { "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/vitest/node_modules/@esbuild/linux-loong64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", - "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", - "cpu": [ - "loong64" - ], + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], "engines": { - "node": ">=12" + "node": "^18.0.0 || >=20.0.0" } }, - "node_modules/vitest/node_modules/@esbuild/linux-mips64el": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", - "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", - "cpu": [ - "mips64el" - ], + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], "engines": { - "node": ">=12" + "node": ">=14.0.0" } }, - "node_modules/vitest/node_modules/@esbuild/linux-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", - "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", - "cpu": [ - "ppc64" - ], + "node_modules/tinyspy": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.6.tgz", + "integrity": "sha512-u8KszXvGfU68hVcZpRHKG28T0krMuv2G5nDhiHaMLen/gIuFEgIJhaJuO69qjnXg5paSrbPMFfx3brNuN8eVSg==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], "engines": { - "node": ">=12" + "node": ">=14.0.0" } }, - "node_modules/vitest/node_modules/@esbuild/linux-riscv64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", - "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", - "cpu": [ - "riscv64" - ], + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "is-number": "^7.0.0" + }, "engines": { - "node": ">=12" + "node": ">=8.0" } }, - "node_modules/vitest/node_modules/@esbuild/linux-s390x": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", - "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", - "cpu": [ - "s390x" - ], + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], "engines": { - "node": ">=12" + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" } }, - "node_modules/vitest/node_modules/@esbuild/linux-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", - "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", - "cpu": [ - "x64" - ], + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } + "license": "Apache-2.0" }, - "node_modules/vitest/node_modules/@esbuild/netbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", - "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", - "cpu": [ - "x64" - ], + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], + "peer": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, "engines": { - "node": ">=12" + "node": ">= 0.8.0" } }, - "node_modules/vitest/node_modules/@esbuild/openbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", - "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", - "cpu": [ - "x64" - ], + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, "engines": { - "node": ">=12" + "node": ">=14.17" } }, - "node_modules/vitest/node_modules/@esbuild/sunos-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", - "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", - "cpu": [ - "x64" - ], + "node_modules/undici-types": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } + "license": "MIT" }, - "node_modules/vitest/node_modules/@esbuild/win32-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", - "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", - "cpu": [ - "arm64" + "node_modules/update-browserslist-db": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.4.tgz", + "integrity": "sha512-t8wonIOdQyOjtPd2VktlBu6QjTVTks6v8bSi7kd0r2Vll8TK1JYDZtRRdbmCHOAMphkZgrzK+L+vXK4pyZ8t4A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } ], - "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" } }, - "node_modules/vitest/node_modules/@esbuild/win32-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", - "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", - "cpu": [ - "ia32" - ], + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" + "license": "BSD-2-Clause", + "peer": true, + "dependencies": { + "punycode": "^2.1.0" } }, - "node_modules/vitest/node_modules/@esbuild/win32-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", - "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", - "cpu": [ - "x64" - ], - "dev": true, + "node_modules/use-sync-external-store": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.7.0.tgz", + "integrity": "sha512-6L+EeigHMQhdaIPNIFUKwfWJSwWFQ8gJbJ2DLOs5sDIegTwR9fRxvnM3uciHKjIZhFz+KAv2emhWMRvDmMcY8A==", "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, - "node_modules/vitest/node_modules/@vitest/mocker": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.9.tgz", - "integrity": "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==", + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "dev": true, - "license": "MIT", + "license": "MIT" + }, + "node_modules/victory-vendor": { + "version": "36.9.2", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz", + "integrity": "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==", + "license": "MIT AND ISC", "dependencies": { - "@vitest/spy": "2.1.9", - "estree-walker": "^3.0.3", - "magic-string": "^0.30.12" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "msw": "^2.4.9", - "vite": "^5.0.0" - }, - "peerDependenciesMeta": { - "msw": { - "optional": true - }, - "vite": { - "optional": true - } + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" } }, - "node_modules/vitest/node_modules/esbuild": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", - "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "node_modules/vite": { + "version": "6.4.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.5.tgz", + "integrity": "sha512-4enwjFfi4IwtEh0w+jPzpLVOFQiPHmmzPxcHXQOyEur47o6u0jE573DmGpjA+7irXoMLYRZD5ZaFUEneS3abPg==", "dev": true, - "hasInstallScript": true, "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.21.5", - "@esbuild/android-arm": "0.21.5", - "@esbuild/android-arm64": "0.21.5", - "@esbuild/android-x64": "0.21.5", - "@esbuild/darwin-arm64": "0.21.5", - "@esbuild/darwin-x64": "0.21.5", - "@esbuild/freebsd-arm64": "0.21.5", - "@esbuild/freebsd-x64": "0.21.5", - "@esbuild/linux-arm": "0.21.5", - "@esbuild/linux-arm64": "0.21.5", - "@esbuild/linux-ia32": "0.21.5", - "@esbuild/linux-loong64": "0.21.5", - "@esbuild/linux-mips64el": "0.21.5", - "@esbuild/linux-ppc64": "0.21.5", - "@esbuild/linux-riscv64": "0.21.5", - "@esbuild/linux-s390x": "0.21.5", - "@esbuild/linux-x64": "0.21.5", - "@esbuild/netbsd-x64": "0.21.5", - "@esbuild/openbsd-x64": "0.21.5", - "@esbuild/sunos-x64": "0.21.5", - "@esbuild/win32-arm64": "0.21.5", - "@esbuild/win32-ia32": "0.21.5", - "@esbuild/win32-x64": "0.21.5" - } - }, - "node_modules/vitest/node_modules/vite": { - "version": "5.4.21", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", - "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", - "dev": true, - "license": "MIT", - "dependencies": { - "esbuild": "^0.21.3", - "postcss": "^8.4.43", - "rollup": "^4.20.0" + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" }, "bin": { "vite": "bin/vite.js" }, "engines": { - "node": "^18.0.0 || >=20.0.0" + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" }, "funding": { "url": "https://github.com/vitejs/vite?sponsor=1" @@ -4592,19 +5587,25 @@ "fsevents": "~2.3.3" }, "peerDependencies": { - "@types/node": "^18.0.0 || >=20.0.0", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", - "terser": "^5.4.0" + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" }, "peerDependenciesMeta": { "@types/node": { "optional": true }, + "jiti": { + "optional": true + }, "less": { "optional": true }, @@ -4625,9 +5626,172 @@ }, "terser": { "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/vitest": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.7.tgz", + "integrity": "sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.7", + "@vitest/mocker": "3.2.7", + "@vitest/pretty-format": "^3.2.7", + "@vitest/runner": "3.2.7", + "@vitest/snapshot": "3.2.7", + "@vitest/spy": "3.2.7", + "@vitest/utils": "3.2.7", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.7", + "@vitest/ui": "3.2.7", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true } } }, + "node_modules/vitest/node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/why-is-node-running": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", @@ -4663,6 +5827,17 @@ "node": ">=0.8" } }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/xlsx": { "version": "0.18.5", "resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz", @@ -4690,6 +5865,20 @@ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "dev": true, "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } } } } diff --git a/web/package.json b/web/package.json index 40827e7..f7a4c80 100644 --- a/web/package.json +++ b/web/package.json @@ -1,33 +1,46 @@ { "name": "finkit-web", - "private": true, "version": "2.0.0", + "private": true, "type": "module", "scripts": { "dev": "vite", "build": "tsc -b && vite build", "preview": "vite preview", - "test": "vitest run" + "test": "vitest run", + "lint": "oxlint", + "lint:fix": "oxlint --fix", + "format": "oxfmt", + "format:check": "oxfmt --check", + "check": "oxlint && oxfmt --check" }, "dependencies": { - "lucide-react": "^0.577.0", - "papaparse": "^5.7.0", - "react": "^18.3.1", - "react-dom": "^18.3.1", - "recharts": "^2.15.4", - "xlsx": "^0.18.5" + "@tanstack/react-query": "5.105.1", + "@tanstack/react-router": "1.170.46", + "lucide-react": "0.577.0", + "papaparse": "5.7.0", + "react": "19.3.1", + "react-dom": "19.3.1", + "recharts": "2.15.4", + "xlsx": "0.18.5" }, "devDependencies": { - "@types/node": "^22.10.0", - "@types/papaparse": "^5.5.2", - "@types/react": "^18.3.31", - "@types/react-dom": "^18.3.7", - "@vitejs/plugin-react": "^4.7.0", - "autoprefixer": "^10.6.2", - "postcss": "^8.5.32", - "tailwindcss": "^3.4.19", - "typescript": "~5.6.2", - "vite": "^6.4.5", - "vitest": "^2.1.9" + "@stylistic/eslint-plugin": "4.4.1", + "@types/node": "24.19.2", + "@types/papaparse": "5.5.2", + "@types/react": "19.3.0", + "@types/react-dom": "19.3.0", + "@vitejs/plugin-react": "4.7.0", + "autoprefixer": "10.6.2", + "oxfmt": "0.75.0", + "oxlint": "1.90.0", + "postcss": "8.5.32", + "tailwindcss": "3.4.19", + "typescript": "6.0.3", + "vite": "6.4.5", + "vitest": "3.2.7" + }, + "engines": { + "node": ">=26.13.0" } } diff --git a/web/public/privacy.html b/web/public/privacy.html index 89238cb..1bdbaa4 100644 --- a/web/public/privacy.html +++ b/web/public/privacy.html @@ -25,31 +25,46 @@

FinKit privacy

Last updated: 1 November 2026

-

FinKit is a personal tool that one person uses to look at their own bank accounts. It is not offered to the public and has no other users.

+

FinKit is self-hosted software: the web app and API run on infrastructure you (or your operator) control. This page describes how that deployment handles data. It is written for a private instance used by the people its operator allows (for example a household), where each person sees only their own data—not a service sold to the public.

-

What FinKit reads

+

No data gathering by FinKit

    -
  • Account names (with only the last digits), balances and transactions of the owner's own accounts.
  • -
  • This happens only after the owner approves access at their bank, through the open-banking service Enable Banking.
  • -
  • FinKit never sees or stores the owner's bank login or password.
  • +
  • FinKit does not include analytics, advertising, crash reporting, or usage telemetry.
  • +
  • There is no FinKit-operated cloud that receives your transactions, categories, or settings.
  • +
  • The application does not phone home to the software authors or any other third party unless you turn on an optional integration listed below.
-

Where the data goes

+

Where your data lives (all self-hosted)

+

“Server” here means the FinKit API you run yourself (Docker container, VPS, or local `npm run dev`)—not a service operated by the FinKit authors. The static web app is served from the same deployment or another host you control.

    -
  • Transactions are shown in the owner's browser. The FinKit server does not keep them.
  • -
  • The server keeps the bank connection itself, encrypted, so it can fetch new transactions later.
  • -
  • The owner's browser may keep a local copy of the transactions so the page can be reopened. It can be switched off and cleared in the app.
  • -
  • Nothing is sold, shared with other people, or used for advertising or profiling.
  • +
  • Your browser — Transaction rows you import or sync are processed and shown here. If you enable “remember session”, a copy can be stored locally in your browser (IndexedDB) so a refresh does not lose work. You can turn that off and clear it in the app.
  • +
  • Your self-hosted API (SQLite file on your disk or Docker volume) — Each person’s email address, merchant→category rules and sheet/settings preferences. If bank sync is enabled, also the encrypted bank connection details needed to keep syncing and the bank transactions synced through it. Each transaction (date, amount, description, names and IBANs) is encrypted at rest with a key held in your server’s configuration, so a copy of the database file or a backup is unreadable without it. Anyone who controls the running server can still read the data. Transactions you import from files are not stored on the server.
  • +
  • Your backups and host — Anyone with access to your machine, server disk, database volume, or browser profile can see what is stored there. Securing that infrastructure is part of self-hosting.
-

Control and deletion

+

What leaves your deployment

+

By default, nothing is exported or uploaded anywhere else. Data only leaves your setup if you configure one of these:

    -
  • A bank connection can be removed in FinKit or in the bank's own app at any time. Removing it in FinKit deletes the stored connection.
  • -
  • Access to each bank expires on its own, after at most 180 days, unless approved again.
  • +
  • Google Sheets (optional) — When you set up a Google service account and export from the app, FinKit sends the summary row you choose to a spreadsheet you control. Google’s terms and privacy policy apply to that copy.
  • +
  • Open Banking (optional) — When bank sync is enabled, your self-hosted API calls Enable Banking and your bank, about twice a day and when you press Sync, and keeps the transactions it fetches for you on your server. When you press Sync, your IP address and browser identifier are passed on to your bank so it can tell a person is present; FinKit does not store them. FinKit never receives or stores your bank login password. Enable Banking’s and your bank’s terms apply to that traffic.
  • +
  • Access gate (optional) — Production setups may sit behind Cloudflare Access or similar; that provider processes sign-in metadata according to its own policy.
+

FinKit does not sell, share, or use your financial data for advertising or profiling.

+ +

Bank connections

+
    +
  • Read-only access: account labels, balances, and transactions after you approve access at your bank (via Enable Banking when sync is on).
  • +
  • Connection secrets are encrypted at rest on your server so sync can continue until you disconnect or consent expires.
  • +
  • You can remove a connection in FinKit or revoke it at your bank. Removing it in FinKit also revokes access at the bank and deletes the connection and the transactions synced through it from your server.
  • +
  • Synced transactions are kept until you delete them (Banks → Stored bank data) or disconnect the bank. Sync history is kept for 90 days and the activity log for a year. To delete everything else (your account, rules and settings), ask this instance’s operator.
  • +
  • Bank consent expires on its own (typically within the period your bank allows, up to about 180 days) unless renewed.
  • +
+ +

File import

+

CSV/XLSX files you upload are parsed in your browser. Merchant names and categories may be sent to your own API when you save learned rules or settings. The transactions themselves stay in your browser; only bank-synced transactions are stored on the server.

Contact

-

Questions about data protection: samson.negedu.a@gmail.com

+

Questions about this deployment: samson.negedu.a@gmail.com