diff --git a/.dockerignore b/.dockerignore index 3541a86..c3672ef 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,22 +1,32 @@ # deps **/node_modules +.pnpm-store # builds apps/api/dist -apps/cdn/dist -apps/socials/.next -apps/api-assets/.next +apps/web/.next +**/out +**/build +**/*.tsbuildinfo # turbo **/.turbo -**/tsconfig.build.tsbuildinfo -# env +# env — the images ship only the examples (the API validates its env against .env.example) **/.env **/.env.* !**/.env.example +# vcs / tooling +.git +.github +.husky +.claude + +# reference material (not part of the app) +example + # misc **/coverage **/*.md -**/.claude +**/.DS_Store diff --git a/.env.example b/.env.example index c6beb12..89a3dd2 100644 --- a/.env.example +++ b/.env.example @@ -1,3 +1,8 @@ +# Root environment — infrastructure + the web app (docker compose reads this file). +# The API's own secrets live in apps/api/.env (see apps/api/.env.example). +# +# cp .env.example .env + # Database (PostgreSQL) DB_HOST="localhost" DB_PORT=5432 @@ -5,17 +10,26 @@ DB_NAME="dropto" DB_USER="root" DB_PASS="password" -# Redis +# Redis (rate limiter) REDIS_PORT=6379 +REDIS_USER="default" REDIS_PASS="password" +REDIS_KEY_PREFIX="dropto:" # URLs APP_URL="http://localhost:3000" -NEXT_PUBLIC_API_URL="http://localhost:4000/api/v1" # Security +# Auth-cookie domain. Use a shared parent in production (e.g. ".example.com") so the web app and the API can both read the cookies. COOKIE_DOMAIN="localhost" -# Web build args +# Branding (optional) — display name of this instance; defaults to "DropTo" when empty. +APP_NAME="DropTo" + +# Web build args — inlined into the browser bundle at build time. NEXT_PUBLIC_GOOGLE_API_KEY="your_google_picker_api_key" NEXT_PUBLIC_GOOGLE_APP_ID="your_google_cloud_project_number" + +# Host ports the containers publish on, bound to 127.0.0.1 (docker compose only). +WEB_PORT=3000 +API_PORT=4000 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..ff702eb --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,64 @@ +name: CI + +# Two-branch flow (dev → prod): fast feedback on every push to dev, and a gate on the PR into prod. +on: + push: + branches: [dev] + pull_request: + branches: [prod] + +# A newer push to the same branch/PR supersedes the run in flight. +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + ci: + name: lint · build · test + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v5 + + # Reads the pnpm version from `packageManager` in package.json, so it cannot drift from local. + - uses: pnpm/action-setup@v4 + + - uses: actions/setup-node@v5 + with: + node-version: 24 + cache: pnpm + + # Turborepo's local cache: unchanged packages replay instead of re-running. + - name: Restore Turborepo cache + uses: actions/cache@v4 + with: + path: .turbo + key: turbo-${{ runner.os }}-${{ github.sha }} + restore-keys: turbo-${{ runner.os }}- + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Lint + run: pnpm lint + + # The lint scripts run eslint with --fix, so a fixable violation would be silently repaired and + # the step would pass. Failing on a dirty tree is what turns lint back into a real gate. + - name: Verify lint changed nothing + run: git diff --exit-code + + - name: Build + run: pnpm build + + - name: Unit tests (api) + run: pnpm --filter api run test + + # Committed template of throwaway values — see the summary for why it is copied, not rebuilt here. + - name: Create the API test environment + run: cp apps/api/.env.test.example apps/api/.env.test + + - name: E2E tests (api) + run: pnpm --filter api run test:e2e diff --git a/README.md b/README.md index 26203db..0130e14 100644 --- a/README.md +++ b/README.md @@ -1,93 +1,262 @@ # DropTo -> Self-hosted file uploader that lets you route uploads through your own storage backends (Google Drive, S3, and more) instead of your app server's disk. - -- [Prerequisites](#Prerequisites) -- [Dependencies](#Dependencies) -- [Projects](#Projects) -- [Installation](#Installation) -- [Run](#Run) - - [Docker](#Docker) -- [Configuration](#Configuration) -- [Deployment](#Deployment) -- [License](#License) +> Self-hosted file uploader that routes uploads through **your own** storage backends — Google Drive +> or any S3-compatible bucket — instead of your app server's disk. + +- [Prerequisites](#prerequisites) +- [Features](#features) +- [Projects](#projects) +- [Installation](#installation) +- [Run](#run) +- [Docker](#docker) +- [Configuration](#configuration) +- [Connecting storage](#connecting-storage) +- [Deployment](#deployment) +- [License](#license) ## Prerequisites -- Node.js 22+ ([Download](https://nodejs.org/en/download/)) -- Container runtime ([Docker (recommended)](https://www.docker.com/), Colima, ...) -- IDE ([VS Code](https://code.visualstudio.com/), WebStorm, ...) +- Node.js 22+ ([Download](https://nodejs.org/en/download/)) — only for running the apps natively - Package manager ([pnpm (recommended)](https://pnpm.io/installation), npm, ...) +- [Docker](https://docs.docker.com/get-started/get-docker/) (Docker Desktop, Colima, ...) — required + for the database, and the simplest way to run the whole stack +- IDE ([VS Code](https://code.visualstudio.com/), WebStorm, ...) +- A Google Cloud project (to connect a Google Drive) and/or S3 credentials — see + [Connecting storage](#connecting-storage) -## Dependencies +## Features -- **Postgres** _(started automatically via docker compose)_ +- **Your storage, not the server's disk** — uploads are streamed straight through to Google Drive or + an S3-compatible bucket (AWS S3, MinIO, Cloudflare R2, ...); nothing is buffered on disk +- **Finder-like workspace** — a three-pane browser (storage sidebar, file list, preview) with a + split view, sorting, search, breadcrumbs, deep links and image previews +- **Drag & drop uploads** — whole folder trees, with duplicate handling (replace / keep both / + cancel), per-file progress and ETA in a floating upload dock, and abortable transfers sized by + `MAX_UPLOAD_BYTES` +- **Storage switcher** — browse Google Drive and S3 side by side and move between them in one click +- **Folder-scoped Drive access** — the owner authorizes specific folders through the Google Picker, + and every single Drive call is validated server-side (ancestor walk) to stay inside that tree; + the authorized roots themselves cannot be deleted +- **Single-operator auth** — username/password from the env, JWT access tokens plus rotating, + DB-backed refresh tokens in httpOnly cookies, with reuse detection +- **Secrets stay encrypted** — the Drive refresh token is AES-256-GCM encrypted in Postgres and the + browser never talks to Google (or the API) directly; everything goes through the Next.js BFF +- **Hardened by default** — a strict CSP and security headers, an IP-keyed rate limiter, read-only + containers running as a non-root user +- **Rebrandable** — set `APP_NAME` and the whole app, its titles and its link previews follow ## Projects -| Name | Description | -| ------------------ | ----------------------------------------------------- | -| [web](./apps/web/) | Next.js front-end — setup page + uploader UI | -| [api](./apps/api/) | NestJS API — Google OAuth, Drive integration, uploads | +| Name | Description | +| -------------------------- | ------------------------------------------------------------------------ | +| [web](./apps/web/) | Next.js front-end — login, workspace, Google Picker setup; the BFF layer | +| [api](./apps/api/) | NestJS API — operator auth, Google OAuth, storage providers, uploads | +| [types](./packages/types/) | `@dropto/types` — the shared API contract between the two | ## Installation 1. Clone the repository and navigate to the root: `cd drop-to/` 2. Install all dependencies: `pnpm install` -3. Copy `.env.example` to `.env` in the monorepo root and in each app directory (`apps/web/`, `apps/api/`) and adjust the values accordingly. -4. Start required services: `docker compose -f docker-compose.infra.yml up -d` - > Run all docker compose commands from the monorepo root. -5. **First run only:** Apply database migrations and generate Prisma client: `pnpm --filter api prisma:migrate && pnpm --filter api prisma:generate` -6. Start all services (see [Run](#Run) section). +3. Create the env files and adjust the values: + +```bash +cp .env.example .env # infrastructure + web (docker compose reads this) +cp apps/api/.env.example apps/api/.env # API secrets +cp apps/web/.env.example apps/web/.env # only for running the web app natively +``` + +> In the Docker deployment the web app gets its settings from compose, so `apps/web/.env` is only +> needed when you run `pnpm dev` / `pnpm build` directly. + +4. Generate the two secrets the API needs and paste them into `apps/api/.env`: + +```bash +node -e "console.log(require('crypto').randomBytes(48).toString('base64url'))" # JWT_ACCESS_SECRET +node -e "console.log(require('crypto').randomBytes(32).toString('hex'))" # TOKEN_ENCRYPTION_KEY +``` + +5. Set `AUTH_USERNAME` / `AUTH_PASSWORD` in `apps/api/.env` — that is the account you log in with. + +From here, either run everything in Docker ([Docker](#docker), recommended) or run the apps natively +([Run](#run)). ## Run -- Development mode (native): `pnpm dev` -- Production mode (native): `pnpm build && pnpm start` -- Production mode (Docker): see [Docker](#Docker) section +Native development, with only the infrastructure in Docker: + +```bash +docker compose up -d dropto-pg dropto-redis # infrastructure only — builds no app image +pnpm --filter api run prisma:migrate:deploy # first run only +pnpm dev # api :4000, web :3000 +``` + +Other scripts (run from the monorepo root): + +- Production build: `pnpm build` +- Lint: `pnpm lint` +- Tests: `pnpm test` +- Format: `pnpm format` + +> Run every `docker compose` and `pnpm` command from the monorepo root. + +## Docker + +The full stack — Postgres, Redis, the API and the web app — is one command. Both apps ship a +multi-stage `Dockerfile` (Nest `dist` / Next.js standalone output, non-root user, health checks) and +the compose file wires them together. -### Docker +```bash +cp .env.example .env # if you have not already +cp apps/api/.env.example apps/api/.env # compose overrides DB_*/REDIS_* from the root .env +docker compose up -d --build # http://localhost:3000 +docker compose logs -f dropto-web # stream logs for a service +docker compose down # stop; the volumes keep your data +``` -Alternatively, you can run each project in a Docker container (recommended for production): +Database migrations run **automatically** on every `up`: the one-shot `dropto-migrate` service +applies them and the API only starts once it has exited successfully. > **Give the Docker VM enough memory.** The API image compiles TypeScript against the large -> `googleapis` type surface and needs ~4 GiB of RAM for `tsc` — allocate **at least ~6 GiB** to your -> Docker VM or the build can OOM and kill the daemon. With Colima: -> `colima start --cpu 4 --memory 8`. +> `googleapis` type surface and needs ~4 GiB for `tsc` — allocate **at least ~6 GiB** to the VM or +> the build can OOM. With Colima: `colima start --cpu 4 --memory 8`. -1. Copy `.env.example` to `.env` in the monorepo root and in each app directory (`apps/web/`, `apps/api/`) and adjust the values accordingly. -2. Build the images: `docker compose build` - > Run all docker compose commands from the monorepo root. -3. **First run only:** Start infrastructure, apply migrations, then start all services: +Building the images without compose: ```bash - docker compose up -d dropto-pg - docker compose run --rm --no-deps dropto-api node_modules/.bin/prisma migrate deploy - docker compose up -d +docker build -f apps/api/Dockerfile -t dropto-api . +docker build -f apps/web/Dockerfile -t dropto-web . \ + --build-arg APP_URL=https://dropto.example.com \ + --build-arg NEXT_PUBLIC_GOOGLE_API_KEY=... \ + --build-arg NEXT_PUBLIC_GOOGLE_APP_ID=... ``` -4. **Subsequent runs:** `docker compose up -d` +`APP_URL` and every `NEXT_PUBLIC_*` value is inlined at **build time** (they feed the browser bundle +and the prerendered metadata), so pass them as build args for any non-localhost deployment and keep +the runtime values identical. Everything else — including `APP_NAME` — is read at runtime, so +renaming the instance or repointing the API needs no rebuild. + +The compose file binds every published port to `127.0.0.1` (the containers are meant to sit behind a +reverse proxy on the same host); drop that prefix in `docker-compose.yml` to expose them on the +network directly — for example to open the workspace from your phone. ## Configuration -| App | Port | Technologies | URL | -| --------- | ---- | -------------- | -------------------------------------------------------------------- | -| **api** | 4000 | NestJS, Prisma | http://localhost:4000 · Swagger (dev): http://localhost:4000/swagger | -| **admin** | 3000 | Next.js | http://localhost:3000 | +| App | Port | Technologies | URL | +| ------- | ---- | ------------------------ | ---------------------------------------------------------------- | +| **web** | 3000 | Next.js, React, Tailwind | http://localhost:3000 · health: http://localhost:3000/api/health | +| **api** | 4000 | NestJS, Prisma | http://localhost:4000 · health: http://localhost:4000/health | + +> Swagger is served at http://localhost:4000/swagger in development only. + +> Infrastructure + +| Description | Values | +| ---------------- | ----------------------------------- | +| **Ports:** | 5432 (Postgres 17), 6379 (Redis 7) | +| **Credentials:** | `root:password`, `default:password` | + +### Root `.env` — infrastructure + web + +| Variable | Description | +| -------------------------------------------------- | --------------------------------------------------------------------------------------- | +| `DB_*` | Postgres name / user / password; creates the DB **and** is handed to the API | +| `REDIS_*` | Redis password, user and key prefix (the rate limiter's store) | +| `APP_URL` | Public origin of the web app — canonical / OpenGraph / manifest URLs. **Build time** | +| `COOKIE_DOMAIN` | Auth-cookie domain. Leave unset — cookies stay host-only on the web app's origin | +| `APP_NAME` | Display name of the instance (optional, default `DropTo`). Runtime | +| `NEXT_PUBLIC_GOOGLE_API_KEY` / `_APP_ID` | Google Picker API key and project number. **Build time** | +| `WEB_PORT` / `API_PORT` / `DB_PORT` / `REDIS_PORT` | Host ports the containers publish on (compose only) | + +### `apps/api/.env` — API + +| Variable | Description | +| -------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | +| `APP_PORT`, `NODE_ENV` | Listen port and environment | +| `WEB_APP_URL`, `CORS_ALLOWED_ORIGINS` | Where the web app lives, and who may call the API | +| `COOKIE_DOMAIN`, `TRUST_PROXY_HOPS` | Cookie domain (leave unset); trusted proxy hops, so the rate limiter sees the real IP | +| `JWT_ACCESS_SECRET` | Signs access tokens (refresh tokens are opaque, hashed in the DB) | +| `TOKEN_ENCRYPTION_KEY` | 64 hex chars — AES-256-GCM key for the stored Drive refresh token | +| `AUTH_USERNAME`, `AUTH_PASSWORD` | The single operator account you log in with | +| `GOOGLE_CLIENT_ID` / `_SECRET` / `_REDIRECT_URI` | Google OAuth client for the Drive owner | +| `S3_ENABLED` (+ `S3_BUCKETS`, `S3_REGION`, `S3_ACCESS_KEY_ID`, `S3_SECRET_ACCESS_KEY`) | S3 backend; `S3_ENDPOINT` / `S3_FORCE_PATH_STYLE` for MinIO / R2 | +| `MAX_UPLOAD_BYTES`, `OFFLINE_TIMEOUT_MS` | Upload size cap and how long a dropped connection may stall | +| `RATE_LIMIT_ENABLED` | Kill switch for the global rate limiter | +| `DB_*`, `REDIS_*` | Only needed when running natively — compose overrides them from the root `.env` | + +## Connecting storage -> Database & Cache +### Google Drive (one-time Google Cloud setup) -| Description | Values | -| ----------------- | ----------------------------------- | -| **Ports:** | 5432, 6379 | -| **Technologies:** | PostgreSQL 17, Redis | -| **Credentials:** | `root:password`, `default:password` | +1. Create a Google Cloud project — note the **project number**, it is the Picker `App ID` + (`NEXT_PUBLIC_GOOGLE_APP_ID`). +2. Enable the **Google Drive API** and the **Google Picker API**. +3. Configure the OAuth consent screen (External), add the scope + `https://www.googleapis.com/auth/drive` and add the Drive owner's email as a test user. +4. Create an **OAuth client ID** (Web application): + - Authorized redirect URI: `/api/v1/google-auth/google/callback` + - Authorized JavaScript origin: the web app's URL + + → `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET`, `GOOGLE_REDIRECT_URI` in `apps/api/.env`. + +5. Create an **API key** restricted to the Picker API → `NEXT_PUBLIC_GOOGLE_API_KEY`. +6. Start the app, log in, and use **Connect Drive** in the sidebar. The owner picks the folders to + share; those become the only folders the app will touch. + +> **A note on the scope.** DropTo asks for the full `.../auth/drive` scope rather than the +> per-file `drive.file`, because `drive.file` cannot see files that were added to a folder +> _directly in Drive_ — only ones the app created or the Picker handed it, which makes a shared +> folder look half-empty. The trade-off is that the grant covers the owner's whole Drive and the +> confinement to the authorized folders is enforced by the API alone, on every request. + +> **Publish the app, or Drive keeps disconnecting.** While the consent screen sits in **Testing**, +> Google expires the refresh token after **7 days** and the workspace shows "Your Google Drive was +> disconnected". Hit **Publish app** (publishing status → _In production_) and the expiry is gone. +> Verification is **not** needed for this: it is only mandatory for _public_ apps using +> sensitive/restricted scopes. Staying unverified in production costs a one-time "Google hasn't +> verified this app" screen (→ _Advanced_ → _Go to app_), a cap of 100 users and no app name/logo on +> the consent screen — all irrelevant for a single-owner instance. Chasing verified status is not +> worth it: `.../auth/drive` is a _restricted_ scope, which pulls in an annual third-party CASA +> security assessment. + +### S3 / S3-compatible + +Set `S3_ENABLED="true"` in `apps/api/.env` and fill in `S3_BUCKETS` (a comma-separated list — each +bucket becomes a browse root), `S3_REGION`, `S3_ACCESS_KEY_ID` and `S3_SECRET_ACCESS_KEY`. For MinIO, +Cloudflare R2 and friends, also set `S3_ENDPOINT` and usually `S3_FORCE_PATH_STYLE="true"`. No UI +step is needed — the buckets show up in the sidebar on the next load. ## Deployment -DropTo is **self-hosted** — you run it on your own infrastructure, at whatever domain(s) or host/port you choose, routing uploads to whatever storage you want (connect any Google Drive via the setup flow and/or configure your own S3 buckets in the API env). There is **no fixed or official URL**. +DropTo is **self-hosted** — you run it on your own infrastructure, at whatever domain(s) you choose. +There is no fixed or official URL. + +Run the bundled compose stack behind a reverse proxy / TLS of your choice (Caddy, Traefik, nginx, +Coolify, Cloudflare Tunnel, ...) and point the env vars at wherever you host it: + +- `APP_URL` → the public web origin (e.g. `https://dropto.example.com`) — **build arg** +- `WEB_APP_URL` and `CORS_ALLOWED_ORIGINS` (API) → the same public web origin +- `GOOGLE_REDIRECT_URI` → `/api/oauth/google/callback`, and the same value in the + Google Cloud OAuth client +- `API_URL` (web) → the API's internal address, e.g. `http://dropto-api:4000/api/v1` +- `COOKIE_DOMAIN` → **leave it unset.** The browser only ever talks to the web app, so the auth + cookies stay host-only on that one origin. Set it only if you deliberately publish the API on its + own host — and note that on a shared parent every sibling subdomain then receives the session + cookie. + +**The API needs no public route.** Every browser request goes to the web app, which talks to the API +server-to-server over the compose network (`API_URL=http://dropto-api:4000/api/v1`) — including both +legs of the Google OAuth handshake, proxied by `apps/web/src/app/api/oauth`. Publishing the API is +optional, and leaving it unpublished is the tighter default. +- `TRUST_PROXY_HOPS` → the number of proxies in front of the API, so the rate limiter keys on the + real client IP + +> **Serve it over HTTPS.** With `NODE_ENV=production` the auth cookies are issued as `Secure`, so a +> browser will only store them on an `https://` origin — plus `http://localhost`, which browsers +> treat as a secure context. Behind plain http on a LAN IP or a real domain, logging in silently +> fails: the login call succeeds and the cookies are dropped. Terminate TLS at your reverse proxy. -Deploy the full stack (web + api + Postgres + Redis) with the bundled compose file (`docker compose up -d`, see [Docker](#Docker)) behind a reverse proxy / TLS of your choice (Caddy, Traefik, nginx, Coolify, Cloudflare Tunnel, …). Everything URL-related is driven by env vars, so point them at wherever you host: +The images expose health checks (`/api/health` on the web app, `/health` on the API) for the +platform's probes, and both run as a non-root user with a read-only root filesystem. ## License diff --git a/apps/api/.env.example b/apps/api/.env.example index cd6dcb1..8f140b6 100644 --- a/apps/api/.env.example +++ b/apps/api/.env.example @@ -9,14 +9,18 @@ WEB_APP_URL="http://localhost:3000" CORS_ALLOWED_ORIGINS="http://localhost:4000,http://localhost:3000" # Security -COOKIE_DOMAIN="localhost" +# Leave this unset. The browser only ever talks to the web app — including both legs of the Google +# OAuth handshake, which it proxies — so the auth cookies stay host-only on that one origin. Set it +# (to a parent both hosts share) ONLY if you deliberately publish this API on its own host, and know +# that on a shared parent every sibling subdomain then receives the operator's session cookie. +# COOKIE_DOMAIN="" + # Trusted proxy hops in front of the API (Express `trust proxy`). Optional, defaults to 1. Set to # match your topology so the rate limiter keys on the real client IP (e.g. Cloudflare Tunnel + # Coolify may need a higher value). # TRUST_PROXY_HOPS=1 # JWT -# node -e "console.log(require('crypto').randomBytes(48).toString('base64url'))" JWT_ACCESS_SECRET="generate_me_access_secret" # Database (PostgreSQL) @@ -36,7 +40,7 @@ REDIS_KEY_PREFIX="dropto:" # Google OAuth GOOGLE_CLIENT_ID="your_google_oauth_client_id" GOOGLE_CLIENT_SECRET="your_google_oauth_client_secret" -GOOGLE_REDIRECT_URI="http://localhost:4000/api/v1/google-auth/google/callback" +GOOGLE_REDIRECT_URI="http://localhost:3000/api/oauth/google/callback" # Rate limiting RATE_LIMIT_ENABLED="true" @@ -61,5 +65,4 @@ S3_ENABLED="false" # S3_FORCE_PATH_STYLE="true" # Token encryption -# node -e "console.log(require('crypto').randomBytes(32).toString('hex'))" TOKEN_ENCRYPTION_KEY="64_hex_character_key_generate_me_see_comment_above_0000000000000000" diff --git a/apps/api/.env.test.example b/apps/api/.env.test.example new file mode 100644 index 0000000..385d8b9 --- /dev/null +++ b/apps/api/.env.test.example @@ -0,0 +1,63 @@ +# Environment for the e2e suite (`pnpm --filter api run test:e2e`), loaded by test/setup-env.ts. +# The unit specs mock every dependency and need none of this; only the e2e suite boots the app. +# +# cp apps/api/.env.test.example apps/api/.env.test +# +# Nothing here reaches a real service: Google and Prisma are mocked in the e2e helpers, so the +# values only have to satisfy the config validation at boot. Use throwaway values. + +# App +APP_PORT=4001 +NODE_ENV="test" + +# URLs +WEB_APP_URL="http://localhost:3000" + +# CORS +CORS_ALLOWED_ORIGINS="http://localhost:3000" + +# Security +COOKIE_DOMAIN="localhost" + +# JWT +JWT_ACCESS_SECRET="test_access_secret" + +# Database (PostgreSQL) — the Prisma client is mocked, so this is never dialled +DB_HOST="localhost" +DB_PORT=5432 +DB_NAME="dropto_test" +DB_USER="root" +DB_PASS="password" + +# Redis — the rate limiter is disabled below, so this is never dialled +REDIS_HOST="localhost" +REDIS_PORT=6379 +REDIS_USER="default" +REDIS_PASS="password" +REDIS_KEY_PREFIX="dropto:test:" + +# Google OAuth — googleapis is mocked in the e2e helpers +GOOGLE_CLIENT_ID="test_client_id" +GOOGLE_CLIENT_SECRET="test_client_secret" +GOOGLE_REDIRECT_URI="http://localhost:3000/api/oauth/google/callback" + +# Rate limiting — off, so tests don't trip the limiter when replaying requests +RATE_LIMIT_ENABLED="false" + +# Auth — test/auth/session.e2e-spec.ts logs in with exactly these credentials +AUTH_USERNAME="test-admin" +AUTH_PASSWORD="test-password" + +# Uploads — deliberately far below the production values (.env.example ships 2 GiB / 30 s). Both are +# load-bearing for the suite, so keep them small when syncing: 1 MiB is what makes the upload +# size-limit path testable at all (test/drive asserts a 400 by sending a body just over it), and a +# short offline timeout keeps the resumable specs fast. +MAX_UPLOAD_BYTES=1048576 # 1 MiB +OFFLINE_TIMEOUT_MS=1000 + +# S3 storage +S3_ENABLED="false" + +# Token encryption — must stay the all-zero key: test/helpers/fixtures.helper.ts encrypts the +# fixture refresh token with it, and the real CryptoService has to be able to decrypt it. +TOKEN_ENCRYPTION_KEY="0000000000000000000000000000000000000000000000000000000000000000" diff --git a/apps/api/Dockerfile b/apps/api/Dockerfile index 4803a46..780c482 100644 --- a/apps/api/Dockerfile +++ b/apps/api/Dockerfile @@ -12,12 +12,16 @@ RUN apk add --no-cache openssl libc6-compat WORKDIR /app ENV CI=true +# No .git in the build context, so the root `prepare` (husky) has nothing to install. +ENV HUSKY=0 +ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0 COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./ COPY apps/api/package.json ./apps/api/ COPY packages/types/package.json ./packages/types/ -RUN npm install -g pnpm +# corepack resolves pnpm from the `packageManager` field, so the image matches the lockfile. +RUN corepack enable RUN pnpm install --frozen-lockfile COPY apps/api ./apps/api @@ -27,9 +31,9 @@ COPY tsconfig.base.json ./ WORKDIR /app/apps/api -# `pnpm run build` runs `prisma generate` first; it loads prisma.config.ts, whose datasource url is -# templated from DB_* — generate doesn't connect, so build-time placeholders suffice (real values -# come from env_file at runtime). +# The Prisma client is generated into src/prisma/generated and committed, so the build only needs +# tsc. The DB_* placeholders keep prisma.config.ts (whose datasource url is templated from them) +# loadable if a Prisma command runs here; real values come from the environment at runtime. ENV DB_USER=build DB_PASS=build DB_HOST=localhost DB_PORT=5432 DB_NAME=build # tsc needs headroom here (the googleapis type surface is large and OOMs the default ~1 GiB @@ -45,12 +49,14 @@ RUN apk add --no-cache openssl libc6-compat WORKDIR /app ENV CI=true ENV NODE_ENV=production +ENV HUSKY=0 +ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0 COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./ COPY apps/api/package.json ./apps/api/ COPY packages/types/package.json ./packages/types/ -RUN npm install -g pnpm +RUN corepack enable # Production dependencies only (Prisma CLI is a runtime dep — migrations run here). RUN pnpm install --frozen-lockfile --prod @@ -67,4 +73,9 @@ WORKDIR /app/apps/api USER node EXPOSE 4000 + +# 127.0.0.1, not localhost: that also resolves to ::1, which the IPv4 listener refuses. +HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \ + CMD wget -qO- http://127.0.0.1:4000/health || exit 1 + CMD ["node", "dist/main"] diff --git a/apps/api/package.json b/apps/api/package.json index f3b2cc0..e100d27 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -1,21 +1,19 @@ { "name": "api", - "version": "1.0.0", "private": true, "scripts": { "build": "nest build", "start": "nest start", "dev": "nest start --watch", "lint": "eslint \"{src,test}/**/*.ts\" --fix", - "test": "jest", - "test:cov": "jest --coverage", - "test:e2e": "jest --config ./test/jest-e2e.json", + "test": "NODE_OPTIONS=--experimental-vm-modules jest", + "test:cov": "NODE_OPTIONS=--experimental-vm-modules jest --coverage", + "test:e2e": "NODE_OPTIONS=--experimental-vm-modules jest --config ./test/jest-e2e.json", "prisma:generate": "prisma generate", "prisma:migrate": "prisma migrate dev", "prisma:migrate:deploy": "prisma migrate deploy", "prisma:studio": "prisma studio", "prisma:reset": "prisma reset", - "prisma:seed": "ts-node --transpile-only src/prisma/seed.ts", "openapi:export": "ts-node --transpile-only -r tsconfig-paths/register src/common/scripts/export-openapi.ts" }, "jest": { @@ -27,7 +25,7 @@ "rootDir": "src", "testRegex": ".*\\.spec\\.ts$", "transformIgnorePatterns": [ - "/node_modules/.pnpm/(?!(cookie|archiver|compress-commons|crc32-stream|is-stream|zip-stream)@)", + "/node_modules/", "\\.pnp\\.[^/]+$" ], "transform": { @@ -54,17 +52,18 @@ } }, "dependencies": { - "@aws-sdk/client-s3": "^3.1098.0", - "@aws-sdk/lib-storage": "^3.1098.0", - "@nestjs/common": "^11.1.28", - "@nestjs/config": "^4.0.4", - "@nestjs/core": "^11.1.28", - "@nestjs/jwt": "^11.0.2", - "@nestjs/platform-express": "^11.1.28", - "@nestjs/swagger": "^11.4.6", + "@aws-sdk/client-s3": "^3.1121.0", + "@aws-sdk/lib-storage": "^3.1121.0", + "@dropto/types": "workspace:*", + "@nestjs/common": "^12.0.1", + "@nestjs/config": "^12.0.0", + "@nestjs/core": "^12.0.1", + "@nestjs/jwt": "^12.0.1", + "@nestjs/platform-express": "^12.0.1", + "@nestjs/swagger": "^12.0.1", "@nestjs/terminus": "^11.1.1", - "@prisma/adapter-pg": "^7.9.1", - "@prisma/client": "^7.9.1", + "@prisma/adapter-pg": "^7.10.0", + "@prisma/client": "^7.10.0", "archiver": "^8.0.0", "busboy": "^1.6.0", "class-transformer": "^0.5.1", @@ -73,27 +72,27 @@ "cookie-parser": "^1.4.7", "dotenv": "^17.4.2", "express": "^5.2.1", - "googleapis": "^173.0.0", + "googleapis": "^176.0.0", "helmet": "^8.3.0", - "ioredis": "^5.11.1", - "prisma": "^7.9.1", + "ioredis": "^6.0.0", + "prisma": "^7.10.0", "rate-limiter-flexible": "^11.2.0", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.2" }, "devDependencies": { - "@nestjs/cli": "^11.0.24", - "@nestjs/schematics": "^11.1.0", - "@nestjs/testing": "^11.1.28", + "@nestjs/cli": "^12.0.0", + "@nestjs/schematics": "^12.0.0", + "@nestjs/testing": "^12.0.1", "@types/archiver": "^8.0.0", "@types/busboy": "^1.5.4", "@types/cookie-parser": "^1.4.10", "@types/express": "^5.0.6", "@types/jest": "^30.0.0", - "@types/node": "^26.1.2", + "@types/node": "^26.4.0", "@types/supertest": "^7.2.1", - "eslint": "^10.8.0", - "jest": "^30.4.2", + "eslint": "^10.9.1", + "jest": "^30.5.0", "prisma-dbml-generator": "^0.12.0", "supertest": "^7.2.2", "ts-jest": "^29.4.12", diff --git a/apps/api/prisma.config.ts b/apps/api/prisma.config.ts index 7896e79..8177c40 100644 --- a/apps/api/prisma.config.ts +++ b/apps/api/prisma.config.ts @@ -5,7 +5,6 @@ export default defineConfig({ schema: 'src/prisma/schema.prisma', migrations: { path: 'src/prisma/migrations', - seed: 'ts-node --transpile-only src/prisma/seed.ts', }, datasource: { url: `postgresql://${env('DB_USER')}:${env('DB_PASS')}@${env('DB_HOST')}:${env('DB_PORT')}/${env('DB_NAME')}`, diff --git a/apps/api/src/app.setup.ts b/apps/api/src/app.setup.ts new file mode 100644 index 0000000..d5fba08 --- /dev/null +++ b/apps/api/src/app.setup.ts @@ -0,0 +1,40 @@ +import { RequestMethod, ValidationPipe } from '@nestjs/common'; +import { NestExpressApplication } from '@nestjs/platform-express'; +import cookieParser from 'cookie-parser'; +import helmet from 'helmet'; + +import { type AppConfig, appConfig } from './config/app.config'; + +/** + * Applies every request-shaping concern the app needs, in the order they must run. + **/ +// Shared with the e2e harness so the two cannot drift; a middleware the test app skips stops being covered. +export function configureApp(app: NestExpressApplication): void { + const appCfg = app.get(appConfig.KEY); + + // Behind a reverse proxy — hop count is configurable so `req.ip` (rate-limit key) resolves to the real client. + app.set('trust proxy', appCfg.trustProxyHops); + + app.setGlobalPrefix(appCfg.apiPrefix, { + exclude: [{ path: 'health', method: RequestMethod.GET }], + }); + + // CSP disabled: JSON API with no first-party HTML, and a default CSP would break dev Swagger UI. + app.use(helmet({ contentSecurityPolicy: false })); + + app.use(cookieParser()); + + app.enableCors({ + origin: appCfg.corsAllowedOrigins, + methods: 'GET,POST,PATCH,DELETE,HEAD,OPTIONS', + credentials: true, + }); + + app.useGlobalPipes( + new ValidationPipe({ + whitelist: true, + forbidNonWhitelisted: true, + transform: true, + }), + ); +} diff --git a/apps/api/src/common/decorators/request-user.decorator.spec.ts b/apps/api/src/common/decorators/request-user.decorator.spec.ts new file mode 100644 index 0000000..094ac47 --- /dev/null +++ b/apps/api/src/common/decorators/request-user.decorator.spec.ts @@ -0,0 +1,51 @@ +import { ExecutionContext, UnauthorizedException } from '@nestjs/common'; +import { ROUTE_ARGS_METADATA } from '@nestjs/common/constants'; +import type { Request } from 'express'; + +import type { RequestUser as RequestUserType } from '@/common/types/auth-user.types'; + +import { RequestUser } from './request-user.decorator'; + +/** + * Extracts the factory out of a param decorator by applying it to a throwaway handler. + **/ +// Nest stores the factory in route-args metadata — the only supported way to reach it directly. +const factoryOf = ( + decorator: typeof RequestUser, +): ((data: unknown, ctx: ExecutionContext) => RequestUserType) => { + class Probe { + handle(@decorator() user: RequestUserType): RequestUserType { + return user; + } + } + + const args = Reflect.getMetadata(ROUTE_ARGS_METADATA, Probe, 'handle') as Record< + string, + { factory: (data: unknown, ctx: ExecutionContext) => RequestUserType } + >; + + return args[Object.keys(args)[0]].factory; +}; + +/** + * Builds an ExecutionContext whose request carries the given user. + **/ +const buildContext = (user?: unknown): ExecutionContext => + ({ + switchToHttp: () => ({ getRequest: () => ({ user }) as Partial }), + }) as unknown as ExecutionContext; + +describe('RequestUser', () => { + const factory = factoryOf(RequestUser); + + it('returns the user the guard attached to the request', () => { + const user = { username: 'operator' }; + + expect(factory(undefined, buildContext(user))).toBe(user); + }); + + it('throws 401 when no user is attached', () => { + // Failing closed keeps a handler from running with an undefined user if a route is ever mounted without the guard. + expect(() => factory(undefined, buildContext(undefined))).toThrow(UnauthorizedException); + }); +}); diff --git a/apps/api/src/common/filters/global-exception.filter.spec.ts b/apps/api/src/common/filters/global-exception.filter.spec.ts index 9e2e31b..2383aea 100644 --- a/apps/api/src/common/filters/global-exception.filter.spec.ts +++ b/apps/api/src/common/filters/global-exception.filter.spec.ts @@ -5,7 +5,9 @@ import { GlobalExceptionFilter } from './global-exception.filter'; /** * Captures the response `status()`/`json()` calls a filter makes. **/ -const buildHost = (): { +const buildHost = ( + url = '/api/v1/x', +): { host: ArgumentsHost; status: jest.Mock; json: jest.Mock; @@ -15,7 +17,7 @@ const buildHost = (): { const host = { switchToHttp: () => ({ getResponse: () => ({ status }), - getRequest: () => ({ method: 'GET', url: '/api/v1/x', ip: '127.0.0.1' }), + getRequest: () => ({ method: 'GET', url, ip: '127.0.0.1' }), }), } as unknown as ArgumentsHost; @@ -27,7 +29,6 @@ describe('GlobalExceptionFilter', () => { beforeEach(() => { filter = new GlobalExceptionFilter(); - // Silence the logger so test output stays clean. jest.spyOn(filter['logger'], 'warn').mockImplementation(() => undefined); jest.spyOn(filter['logger'], 'error').mockImplementation(() => undefined); }); @@ -75,4 +76,28 @@ describe('GlobalExceptionFilter', () => { filter.catch(new Error('unknown'), buildHost().host); expect(error).toHaveBeenCalledTimes(1); }); + + it('redacts bearer-grade query values before logging the URL', () => { + const warn = jest.spyOn(filter['logger'], 'warn'); + const { host } = buildHost( + '/google-auth/google/callback?code=4/secret&state=nonce&scope=drive', + ); + + filter.catch(new NotFoundException('nope'), host); + + const logged = warn.mock.calls[0][0] as string; + expect(logged).not.toContain('4/secret'); + expect(logged).not.toContain('nonce'); + expect(logged).toContain('code=%5Bredacted%5D'); + // Non-sensitive params survive, so the log is still useful for debugging. + expect(logged).toContain('scope=drive'); + }); + + it('leaves a query-less URL untouched', () => { + const warn = jest.spyOn(filter['logger'], 'warn'); + + filter.catch(new NotFoundException('nope'), buildHost('/api/v1/storage/drive/folders').host); + + expect(warn.mock.calls[0][0]).toContain('/api/v1/storage/drive/folders'); + }); }); diff --git a/apps/api/src/common/filters/global-exception.filter.ts b/apps/api/src/common/filters/global-exception.filter.ts index 019fce5..5b8afd9 100644 --- a/apps/api/src/common/filters/global-exception.filter.ts +++ b/apps/api/src/common/filters/global-exception.filter.ts @@ -1,6 +1,32 @@ import { ArgumentsHost, Catch, ExceptionFilter, HttpException, Logger } from '@nestjs/common'; import type { Request, Response } from 'express'; +/** + * Query params whose values must never reach the logs. + **/ +// The callback's `code` and the redirect's `ownerToken` are bearer-grade, and logs outlive both. +const REDACTED_QUERY_PARAMS = new Set(['code', 'state', 'ownertoken', 'token', 'access_token']); + +/** + * Request URL with sensitive query values masked, for logging. + **/ +const safeUrl = (url: string): string => { + const [path, query] = url.split('?'); + + if (!query) { + return path; + } + + const params = new URLSearchParams(query); + for (const key of [...params.keys()]) { + if (REDACTED_QUERY_PARAMS.has(key.toLowerCase())) { + params.set(key, '[redacted]'); + } + } + + return `${path}?${params.toString()}`; +}; + /** * Normalizes every exception to `{ status, message }`; unknown errors → 500. **/ @@ -20,14 +46,14 @@ export class GlobalExceptionFilter implements ExceptionFilter { const log = status >= 500 ? this.logger.error.bind(this.logger) : this.logger.warn.bind(this.logger); - log(`[${status}] ${request.method} ${request.url} - ${JSON.stringify(message)}`); + log(`[${status}] ${request.method} ${safeUrl(request.url)} - ${JSON.stringify(message)}`); response.status(status).json({ status, message }); return; } this.logger.error( - `Unhandled exception at ${request.method} ${request.url} from ${request.ip}`, + `Unhandled exception at ${request.method} ${safeUrl(request.url)} from ${request.ip}`, exception instanceof Error ? exception : String(exception), ); diff --git a/apps/api/src/common/services/crypto/crypto.service.spec.ts b/apps/api/src/common/services/crypto/crypto.service.spec.ts index dedad0f..a1e1059 100644 --- a/apps/api/src/common/services/crypto/crypto.service.spec.ts +++ b/apps/api/src/common/services/crypto/crypto.service.spec.ts @@ -58,7 +58,6 @@ describe('CryptoService', () => { it('rejects a tampered ciphertext (GCM auth tag mismatch)', () => { const [iv, authTag, data] = service.encrypt('tamper-me').split(':'); - // Flip the first byte of the ciphertext. const flipped = (data[0] === 'a' ? 'b' : 'a') + data.slice(1); expect(() => service.decrypt(`${iv}:${authTag}:${flipped}`)).toThrow(); diff --git a/apps/api/src/common/services/rate-limit/guards/rate-limit.guard.spec.ts b/apps/api/src/common/services/rate-limit/guards/rate-limit.guard.spec.ts new file mode 100644 index 0000000..6948c8d --- /dev/null +++ b/apps/api/src/common/services/rate-limit/guards/rate-limit.guard.spec.ts @@ -0,0 +1,209 @@ +import { BadRequestException, ExecutionContext, HttpException } from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; +import type { Request } from 'express'; + +import type { RateLimitConfig } from '@/config/rate-limit.config'; + +import { RateLimit } from '../decorators/rate-limit.decorator'; +import { SkipRateLimit } from '../decorators/skip-rate-limit.decorator'; +import type { RateLimitHelpers } from '../helpers/rate-limit.helpers'; +import { RateLimitGuard } from './rate-limit.guard'; + +// Real decorators through a real Reflector: a mocked one would pass even on the wrong metadata key. +class PlainController { + plain(): void {} + + @SkipRateLimit() + skipped(): void {} + + @RateLimit({ points: 2, duration: 30, blockDuration: 300, errorMessage: 'Slow down.' }) + limited(): void {} +} + +@SkipRateLimit() +class SkippedController { + plain(): void {} +} + +@RateLimit({ points: 7, duration: 70 }) +class RuledController { + plain(): void {} + + @RateLimit({ points: 1, duration: 10 }) + stricter(): void {} +} + +/** + * Builds an ExecutionContext pointing at the given class/handler with the given request shape. + **/ +const buildContext = ( + clazz: unknown, + handler: unknown, + request: Partial = {}, +): ExecutionContext => + ({ + getHandler: () => handler, + getClass: () => clazz, + switchToHttp: () => ({ + getRequest: () => ({ method: 'post', path: '/things', ip: '1.2.3.4', ...request }), + }), + }) as unknown as ExecutionContext; + +describe('RateLimitGuard', () => { + let helpers: { resolveSubjectKey: jest.Mock; getLimiter: jest.Mock }; + let consume: jest.Mock; + let cfg: RateLimitConfig; + + const buildGuard = () => + new RateLimitGuard(new Reflector(), helpers as unknown as RateLimitHelpers, cfg); + + beforeEach(() => { + consume = jest.fn().mockResolvedValue(undefined); + helpers = { + resolveSubjectKey: jest.fn().mockReturnValue('ip:1.2.3.4'), + getLimiter: jest.fn().mockReturnValue({ consume }), + }; + cfg = { enabled: true, defaultPoints: 300, defaultDuration: 60 }; + }); + + describe('opt-outs', () => { + it('passes everything through when the kill-switch is off', async () => { + cfg = { ...cfg, enabled: false }; + + const context = buildContext(PlainController, PlainController.prototype.plain); + + await expect(buildGuard().canActivate(context)).resolves.toBe(true); + expect(helpers.getLimiter).not.toHaveBeenCalled(); + expect(consume).not.toHaveBeenCalled(); + }); + + it('skips a handler marked @SkipRateLimit', async () => { + const context = buildContext(PlainController, PlainController.prototype.skipped); + + await expect(buildGuard().canActivate(context)).resolves.toBe(true); + expect(consume).not.toHaveBeenCalled(); + }); + + it('skips every handler of a class marked @SkipRateLimit', async () => { + const context = buildContext(SkippedController, SkippedController.prototype.plain); + + await expect(buildGuard().canActivate(context)).resolves.toBe(true); + expect(consume).not.toHaveBeenCalled(); + }); + }); + + describe('rule resolution', () => { + it('falls back to the configured default when nothing is decorated', async () => { + const context = buildContext(PlainController, PlainController.prototype.plain); + + await buildGuard().canActivate(context); + + expect(helpers.getLimiter).toHaveBeenCalledWith( + expect.objectContaining({ rule: { points: 300, duration: 60 } }), + ); + }); + + it('uses the class rule when the handler has none', async () => { + const context = buildContext(RuledController, RuledController.prototype.plain); + + await buildGuard().canActivate(context); + + expect(helpers.getLimiter).toHaveBeenCalledWith( + expect.objectContaining({ rule: { points: 7, duration: 70 } }), + ); + }); + + it('prefers the handler rule over the class rule', async () => { + const context = buildContext(RuledController, RuledController.prototype.stricter); + + await buildGuard().canActivate(context); + + expect(helpers.getLimiter).toHaveBeenCalledWith( + expect.objectContaining({ rule: { points: 1, duration: 10 } }), + ); + }); + }); + + describe('bucket identity', () => { + it('uppercases the method and prefers the route path over the resolved one', async () => { + const context = buildContext(PlainController, PlainController.prototype.plain, { + method: 'patch', + path: '/things/abc-123', + route: { path: '/things/:id' }, + } as Partial); + + await buildGuard().canActivate(context); + + // The route pattern, not the concrete URL — otherwise every id would get its own bucket. + expect(helpers.getLimiter).toHaveBeenCalledWith( + expect.objectContaining({ method: 'PATCH', path: '/things/:id' }), + ); + }); + + it('falls back to UNKNOWN when neither method nor path can be determined', async () => { + const context = buildContext(PlainController, PlainController.prototype.plain, { + method: undefined, + path: undefined, + } as unknown as Partial); + + await buildGuard().canActivate(context); + + expect(helpers.getLimiter).toHaveBeenCalledWith( + expect.objectContaining({ method: 'UNKNOWN', path: 'UNKNOWN' }), + ); + }); + + it('consumes a point against the resolved subject key', async () => { + const context = buildContext(PlainController, PlainController.prototype.plain); + + await expect(buildGuard().canActivate(context)).resolves.toBe(true); + expect(consume).toHaveBeenCalledWith('ip:1.2.3.4'); + }); + }); + + describe('rejections', () => { + it('rejects with 400 when no subject key can be derived', async () => { + helpers.resolveSubjectKey.mockReturnValue(null); + + const context = buildContext(PlainController, PlainController.prototype.plain); + + await expect(buildGuard().canActivate(context)).rejects.toBeInstanceOf(BadRequestException); + expect(consume).not.toHaveBeenCalled(); + }); + + it('turns an exhausted bucket into a 429', async () => { + consume.mockRejectedValue(new Error('consumed')); + + const context = buildContext(PlainController, PlainController.prototype.plain); + + await expect(buildGuard().canActivate(context)).rejects.toMatchObject({ + status: 429, + message: 'Too many requests, try again later.', + }); + }); + + it("uses the rule's own message when it declares one", async () => { + consume.mockRejectedValue(new Error('consumed')); + + const context = buildContext(PlainController, PlainController.prototype.limited); + + await expect(buildGuard().canActivate(context)).rejects.toMatchObject({ + status: 429, + message: 'Slow down.', + }); + }); + + it('never leaks the limiter error itself (it can carry bucket internals)', async () => { + consume.mockRejectedValue({ msBeforeNext: 42_000, remainingPoints: 0 }); + + const context = buildContext(PlainController, PlainController.prototype.plain); + + const error = await buildGuard() + .canActivate(context) + .catch((e: unknown) => e); + + expect(error).toBeInstanceOf(HttpException); + expect(JSON.stringify(error)).not.toContain('msBeforeNext'); + }); + }); +}); diff --git a/apps/api/src/common/utils/ttl-cache.functions.spec.ts b/apps/api/src/common/utils/ttl-cache.functions.spec.ts new file mode 100644 index 0000000..401b79b --- /dev/null +++ b/apps/api/src/common/utils/ttl-cache.functions.spec.ts @@ -0,0 +1,55 @@ +import { TtlCache } from './ttl-cache.functions'; + +describe('TtlCache', () => { + afterEach(() => { + jest.useRealTimers(); + }); + + it('returns a stored value before it expires', () => { + const cache = new TtlCache(1000, 10); + + cache.set('a', 'value'); + + expect(cache.get('a')).toBe('value'); + }); + + it('returns undefined for a key that was never set', () => { + expect(new TtlCache(1000, 10).get('missing')).toBeUndefined(); + }); + + it('drops a value once its TTL has passed', () => { + jest.useFakeTimers(); + + const cache = new TtlCache(1000, 10); + cache.set('a', 'value'); + + jest.advanceTimersByTime(1001); + + expect(cache.get('a')).toBeUndefined(); + }); + + it('evicts the oldest entry when the cap is reached', () => { + const cache = new TtlCache(1000, 2); + + cache.set('a', '1'); + cache.set('b', '2'); + cache.set('c', '3'); + + expect(cache.get('a')).toBeUndefined(); + expect(cache.get('b')).toBe('2'); + expect(cache.get('c')).toBe('3'); + }); + + it('refreshes the TTL when an existing key is written again', () => { + jest.useFakeTimers(); + + const cache = new TtlCache(1000, 10); + cache.set('a', 'first'); + + jest.advanceTimersByTime(800); + cache.set('a', 'second'); + jest.advanceTimersByTime(800); + + expect(cache.get('a')).toBe('second'); + }); +}); diff --git a/apps/api/src/common/utils/ttl-cache.functions.ts b/apps/api/src/common/utils/ttl-cache.functions.ts new file mode 100644 index 0000000..e2f1177 --- /dev/null +++ b/apps/api/src/common/utils/ttl-cache.functions.ts @@ -0,0 +1,47 @@ +/** An entry plus the timestamp after which it must be recomputed. */ +interface Entry { + value: T; + expiresAt: number; +} + +/** + * A tiny in-process cache with a per-entry TTL and a hard size cap. + **/ +// In-process on purpose: one API container, and entries are cheap enough that a restart losing them costs nothing. +export class TtlCache { + private readonly entries = new Map>(); + + constructor( + private readonly ttlMs: number, + private readonly maxEntries: number, + ) {} + + get(key: string): T | undefined { + const entry = this.entries.get(key); + + if (!entry) { + return undefined; + } + + if (entry.expiresAt <= Date.now()) { + this.entries.delete(key); + + return undefined; + } + + return entry.value; + } + + set(key: string, value: T): void { + // Drop the oldest insertion once full; Map preserves insertion order. + if (this.entries.size >= this.maxEntries) { + const oldest = this.entries.keys().next(); + + if (!oldest.done) { + this.entries.delete(oldest.value); + } + } + + this.entries.set(key, { value, expiresAt: Date.now() + this.ttlMs }); + } +} diff --git a/apps/api/src/config/app.config.spec.ts b/apps/api/src/config/app.config.spec.ts new file mode 100644 index 0000000..6d51b11 --- /dev/null +++ b/apps/api/src/config/app.config.spec.ts @@ -0,0 +1,67 @@ +import { appConfig } from './app.config'; + +const KEYS = ['APP_PORT', 'NODE_ENV', 'CORS_ALLOWED_ORIGINS', 'WEB_APP_URL', 'TRUST_PROXY_HOPS']; + +describe('appConfig', () => { + let saved: Record; + + beforeEach(() => { + saved = {}; + for (const key of KEYS) { + saved[key] = process.env[key]; + } + Object.assign(process.env, { + APP_PORT: '4000', + NODE_ENV: 'test', + CORS_ALLOWED_ORIGINS: 'http://localhost:3000', + WEB_APP_URL: 'http://localhost:3000', + }); + delete process.env['TRUST_PROXY_HOPS']; + }); + + afterEach(() => { + for (const key of KEYS) { + if (saved[key] === undefined) delete process.env[key]; + else process.env[key] = saved[key]; + } + }); + + it('parses the port as a number', () => { + process.env['APP_PORT'] = '4321'; + + expect(appConfig().port).toBe(4321); + }); + + it('derives the environment flags from NODE_ENV', () => { + process.env['NODE_ENV'] = 'production'; + expect(appConfig()).toMatchObject({ isProduction: true, isDevelopment: false }); + + process.env['NODE_ENV'] = 'development'; + expect(appConfig()).toMatchObject({ isProduction: false, isDevelopment: true }); + + // Neither flag is true under `test`, which is what keeps Swagger off in the e2e suite. + process.env['NODE_ENV'] = 'test'; + expect(appConfig()).toMatchObject({ isProduction: false, isDevelopment: false }); + }); + + it('splits and trims the CORS allowlist', () => { + process.env['CORS_ALLOWED_ORIGINS'] = 'https://a.test, https://b.test ,https://c.test'; + + expect(appConfig().corsAllowedOrigins).toEqual([ + 'https://a.test', + 'https://b.test', + 'https://c.test', + ]); + }); + + // Security-relevant: the hop count decides which X-Forwarded-For entry becomes `req.ip`, the rate-limit key. + it('defaults the trusted proxy hop count to 1', () => { + expect(appConfig().trustProxyHops).toBe(1); + }); + + it('honours an explicit TRUST_PROXY_HOPS', () => { + process.env['TRUST_PROXY_HOPS'] = '2'; + + expect(appConfig().trustProxyHops).toBe(2); + }); +}); diff --git a/apps/api/src/config/crypto.config.spec.ts b/apps/api/src/config/crypto.config.spec.ts new file mode 100644 index 0000000..ccb4447 --- /dev/null +++ b/apps/api/src/config/crypto.config.spec.ts @@ -0,0 +1,49 @@ +import { cryptoConfig } from './crypto.config'; + +const ZERO_KEY = '0'.repeat(64); + +describe('cryptoConfig', () => { + let saved: string | undefined; + + beforeEach(() => { + saved = process.env['TOKEN_ENCRYPTION_KEY']; + }); + + afterEach(() => { + if (saved === undefined) delete process.env['TOKEN_ENCRYPTION_KEY']; + else process.env['TOKEN_ENCRYPTION_KEY'] = saved; + }); + + it('decodes 64 hex characters into the 32-byte AES-256 key', () => { + process.env['TOKEN_ENCRYPTION_KEY'] = ZERO_KEY; + + const cfg = cryptoConfig(); + + expect(cfg.tokenEncryptionKey).toBeInstanceOf(Buffer); + expect(cfg.tokenEncryptionKey).toHaveLength(32); + }); + + it('accepts uppercase hex', () => { + process.env['TOKEN_ENCRYPTION_KEY'] = 'A'.repeat(64); + + expect(cryptoConfig().tokenEncryptionKey).toHaveLength(32); + }); + + // Buffer.from('hex') truncates at the first invalid pair instead of throwing, so a short key would silently be a weak one. + it.each([ + ['too short', 'abcd'], + ['too long', '0'.repeat(65)], + ['non-hex characters', 'z'.repeat(64)], + ['empty', ''], + ])('rejects a key that is %s', (_label, value) => { + process.env['TOKEN_ENCRYPTION_KEY'] = value; + + expect(() => cryptoConfig()).toThrow(/64 hex characters/i); + }); + + it('rejects a missing key rather than booting without encryption', () => { + delete process.env['TOKEN_ENCRYPTION_KEY']; + + expect(() => cryptoConfig()).toThrow(/64 hex characters/i); + }); +}); diff --git a/apps/api/src/main.ts b/apps/api/src/main.ts index d75cc42..f5f0cbd 100644 --- a/apps/api/src/main.ts +++ b/apps/api/src/main.ts @@ -1,11 +1,9 @@ -import { RequestMethod, ValidationPipe } from '@nestjs/common'; import { NestFactory } from '@nestjs/core'; import { NestExpressApplication } from '@nestjs/platform-express'; import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; -import cookieParser from 'cookie-parser'; -import helmet from 'helmet'; import { AppModule } from './app.module'; +import { configureApp } from './app.setup'; import { type AppConfig, appConfig } from './config/app.config'; async function bootstrap() { @@ -13,23 +11,8 @@ async function bootstrap() { const appCfg = app.get(appConfig.KEY); - // Behind a reverse proxy — hop count is configurable so `req.ip` (rate-limit key) resolves to the real client. - app.set('trust proxy', appCfg.trustProxyHops); - - app.setGlobalPrefix(appCfg.apiPrefix, { - exclude: [{ path: 'health', method: RequestMethod.GET }], - }); - - // CSP disabled: JSON API with no first-party HTML, and a default CSP would break dev Swagger UI. - app.use(helmet({ contentSecurityPolicy: false })); - - app.use(cookieParser()); - - app.enableCors({ - origin: appCfg.corsAllowedOrigins, - methods: 'GET,POST,PATCH,DELETE,HEAD,OPTIONS', - credentials: true, - }); + // Everything a request passes through lives in `configureApp`, shared with the e2e harness. + configureApp(app); if (appCfg.isDevelopment) { const config = new DocumentBuilder() @@ -41,14 +24,6 @@ async function bootstrap() { SwaggerModule.setup('swagger', app, documentFactory, { jsonDocumentUrl: 'swagger/json' }); } - app.useGlobalPipes( - new ValidationPipe({ - whitelist: true, - forbidNonWhitelisted: true, - transform: true, - }), - ); - await app.listen(appCfg.port); } diff --git a/apps/api/src/modules/auth/entities/auth-user.entity.ts b/apps/api/src/modules/auth/entities/auth-user.entity.ts index bf4a554..3753db7 100644 --- a/apps/api/src/modules/auth/entities/auth-user.entity.ts +++ b/apps/api/src/modules/auth/entities/auth-user.entity.ts @@ -1,6 +1,7 @@ +import type { AuthUser } from '@dropto/types'; import { ApiProperty } from '@nestjs/swagger'; -export class AuthUserEntity { +export class AuthUserEntity implements AuthUser { @ApiProperty({ type: 'string', description: 'Username of the authenticated operator' }) username: string; } diff --git a/apps/api/src/modules/google-auth/entities/allowed-folder.entity.ts b/apps/api/src/modules/google-auth/entities/allowed-folder.entity.ts index 613b914..6aa06b4 100644 --- a/apps/api/src/modules/google-auth/entities/allowed-folder.entity.ts +++ b/apps/api/src/modules/google-auth/entities/allowed-folder.entity.ts @@ -1,6 +1,7 @@ +import type { AllowedFolder } from '@dropto/types'; import { ApiProperty } from '@nestjs/swagger'; -export class AllowedFolderEntity { +export class AllowedFolderEntity implements AllowedFolder { @ApiProperty({ type: 'string', description: 'Internal record id' }) id: string; @@ -15,5 +16,22 @@ export class AllowedFolderEntity { format: 'date-time', description: 'When the folder was authorized', }) + createdAt: string; +} + +/** + * Maps a Prisma allowed-folder row onto the wire entity (Date → ISO string). + **/ +export function toAllowedFolderEntity(row: { + id: string; + folderId: string; + name: string; createdAt: Date; +}): AllowedFolderEntity { + return { + id: row.id, + folderId: row.folderId, + name: row.name, + createdAt: row.createdAt.toISOString(), + }; } diff --git a/apps/api/src/modules/google-auth/google-auth.service.spec.ts b/apps/api/src/modules/google-auth/google-auth.service.spec.ts index 00861ff..236e3b3 100644 --- a/apps/api/src/modules/google-auth/google-auth.service.spec.ts +++ b/apps/api/src/modules/google-auth/google-auth.service.spec.ts @@ -35,7 +35,7 @@ describe('GoogleAuthService', () => { findFirst: jest.Mock; delete: jest.Mock; }; - allowedFolder: { upsert: jest.Mock; findMany: jest.Mock }; + allowedFolder: { upsert: jest.Mock; findMany: jest.Mock; deleteMany: jest.Mock }; $transaction: jest.Mock; }; let crypto: { encrypt: jest.Mock; decrypt: jest.Mock }; @@ -64,7 +64,11 @@ describe('GoogleAuthService', () => { findFirst: jest.fn(), delete: jest.fn(), }, - allowedFolder: { upsert: jest.fn(), findMany: jest.fn() }, + allowedFolder: { + upsert: jest.fn(), + findMany: jest.fn(), + deleteMany: jest.fn().mockResolvedValue({ count: 1 }), + }, $transaction: jest.fn(), }; crypto = { encrypt: jest.fn(), decrypt: jest.fn() }; @@ -206,16 +210,21 @@ describe('GoogleAuthService', () => { describe('saveAllowedFolders', () => { it('upserts each selected folder in a transaction and returns the updated list', async () => { + // Prisma hands back a Date; the entity exposes the ISO string the wire contract declares. const saved = [ { id: 'f1', folderId: 'drive-1', name: 'Photos', createdAt: new Date('2026-01-01') }, ]; + const expected = saved.map((folder) => ({ + ...folder, + createdAt: folder.createdAt.toISOString(), + })); prisma.driveAccount.findFirst.mockResolvedValue({ id: 'acc-1' }); prisma.$transaction.mockResolvedValue([]); prisma.allowedFolder.findMany.mockResolvedValue(saved); const dto = { folders: [{ folderId: 'drive-1', name: 'Photos' }] }; - await expect(service.saveAllowedFolders(dto)).resolves.toEqual(saved); + await expect(service.saveAllowedFolders(dto)).resolves.toEqual(expected); expect(prisma.allowedFolder.upsert).toHaveBeenCalledWith( expect.objectContaining({ where: { driveAccountId_folderId: { driveAccountId: 'acc-1', folderId: 'drive-1' } }, @@ -240,6 +249,10 @@ describe('GoogleAuthService', () => { const allowedFolders = [ { id: 'f1', folderId: 'drive-1', name: 'Photos', createdAt: new Date('2026-01-01') }, ]; + const expectedFolders = allowedFolders.map((folder) => ({ + ...folder, + createdAt: folder.createdAt.toISOString(), + })); prisma.driveAccount.findFirst.mockResolvedValue({ email: 'owner@gmail.com', allowedFolders, @@ -248,9 +261,108 @@ describe('GoogleAuthService', () => { await expect(service.getStatus()).resolves.toEqual({ connected: true, email: 'owner@gmail.com', - allowedFolders, + allowedFolders: expectedFolders, + }); + }); + }); + + // The owner token is what separates "the operator is logged in" from "the operator controls the account". + describe('issueOwnerToken / isVerifiedOwner', () => { + /** + * Makes `decrypt` return the given owner-token payload verbatim. + **/ + const decryptsTo = (payload: unknown) => + crypto.decrypt.mockReturnValue(JSON.stringify(payload)); + + it('issues a token bound to the email and an expiry', () => { + crypto.encrypt.mockReturnValue('sealed'); + + expect(service.issueOwnerToken('owner@gmail.com')).toBe('sealed'); + + const payload = JSON.parse((crypto.encrypt.mock.calls[0] as [string])[0]) as { + email: string; + exp: number; + }; + expect(payload.email).toBe('owner@gmail.com'); + expect(payload.exp).toBeGreaterThan(Date.now()); + }); + + it('verifies a token whose email matches the connected account', async () => { + decryptsTo({ email: 'owner@gmail.com', exp: Date.now() + 60_000 }); + prisma.driveAccount.findFirst.mockResolvedValue({ email: 'owner@gmail.com' }); + + await expect(service.isVerifiedOwner('sealed')).resolves.toBe(true); + }); + + it('rejects a token for a different account than the one connected', async () => { + // Reconnecting to another Google account must invalidate the previous owner's proof. + decryptsTo({ email: 'previous@gmail.com', exp: Date.now() + 60_000 }); + prisma.driveAccount.findFirst.mockResolvedValue({ email: 'current@gmail.com' }); + + await expect(service.isVerifiedOwner('sealed')).resolves.toBe(false); + }); + + it('rejects an expired token', async () => { + decryptsTo({ email: 'owner@gmail.com', exp: Date.now() - 1 }); + + await expect(service.isVerifiedOwner('sealed')).resolves.toBe(false); + expect(prisma.driveAccount.findFirst).not.toHaveBeenCalled(); + }); + + it('rejects a token with no expiry rather than treating it as eternal', async () => { + decryptsTo({ email: 'owner@gmail.com' }); + + await expect(service.isVerifiedOwner('sealed')).resolves.toBe(false); + }); + + it('rejects a token that does not decrypt (forged or tampered)', async () => { + crypto.decrypt.mockImplementation(() => { + throw new Error('bad auth tag'); + }); + + await expect(service.isVerifiedOwner('forged')).resolves.toBe(false); + }); + + it('rejects a decryptable token that is not the expected JSON shape', async () => { + crypto.decrypt.mockReturnValue('not json at all'); + + await expect(service.isVerifiedOwner('sealed')).resolves.toBe(false); + }); + + it('rejects an absent token without touching crypto or the database', async () => { + await expect(service.isVerifiedOwner(undefined)).resolves.toBe(false); + expect(crypto.decrypt).not.toHaveBeenCalled(); + expect(prisma.driveAccount.findFirst).not.toHaveBeenCalled(); + }); + + it('rejects a valid token when no account is connected any more', async () => { + decryptsTo({ email: 'owner@gmail.com', exp: Date.now() + 60_000 }); + prisma.driveAccount.findFirst.mockResolvedValue(null); + + await expect(service.isVerifiedOwner('sealed')).resolves.toBe(false); + }); + }); + + describe('removeAllowedFolder', () => { + it('deletes the folder scoped to the active account', async () => { + prisma.driveAccount.findFirst.mockResolvedValue({ id: 'acc-1' }); + + await service.removeAllowedFolder('drive-1'); + + // Scoped by account, so an id from another account can never be unauthorized through this. + expect(prisma.allowedFolder.deleteMany).toHaveBeenCalledWith({ + where: { driveAccountId: 'acc-1', folderId: 'drive-1' }, }); }); + + it('fails when no account is connected', async () => { + prisma.driveAccount.findFirst.mockResolvedValue(null); + + await expect(service.removeAllowedFolder('drive-1')).rejects.toBeInstanceOf( + NotFoundException, + ); + expect(prisma.allowedFolder.deleteMany).not.toHaveBeenCalled(); + }); }); describe('disconnect', () => { diff --git a/apps/api/src/modules/google-auth/google-auth.service.ts b/apps/api/src/modules/google-auth/google-auth.service.ts index 74dcccf..a1e75e3 100644 --- a/apps/api/src/modules/google-auth/google-auth.service.ts +++ b/apps/api/src/modules/google-auth/google-auth.service.ts @@ -12,7 +12,7 @@ import { type GoogleConfig, googleConfig } from '@/config/google.config'; import { PrismaService } from '@/prisma/prisma.service'; import { SaveFoldersDto } from './dto/save-folders.dto'; -import { AllowedFolderEntity } from './entities/allowed-folder.entity'; +import { AllowedFolderEntity, toAllowedFolderEntity } from './entities/allowed-folder.entity'; import { DriveAccountStatusEntity } from './entities/drive-account-status.entity'; import { DRIVE_OWNER_TTL_MS } from './google-auth.constants'; @@ -237,11 +237,13 @@ export class GoogleAuthService { ), ); - return this.prismaService.allowedFolder.findMany({ + const folders = await this.prismaService.allowedFolder.findMany({ where: { driveAccountId }, select: { id: true, folderId: true, name: true, createdAt: true }, orderBy: { createdAt: 'asc' }, }); + + return folders.map(toAllowedFolderEntity); } /** @@ -265,7 +267,7 @@ export class GoogleAuthService { return { connected: true, email: account.email, - allowedFolders: account.allowedFolders, + allowedFolders: account.allowedFolders.map(toAllowedFolderEntity), }; } diff --git a/apps/api/src/modules/google-auth/guards/drive-owner.guard.spec.ts b/apps/api/src/modules/google-auth/guards/drive-owner.guard.spec.ts new file mode 100644 index 0000000..fc9bfc5 --- /dev/null +++ b/apps/api/src/modules/google-auth/guards/drive-owner.guard.spec.ts @@ -0,0 +1,61 @@ +import { ExecutionContext, ForbiddenException } from '@nestjs/common'; +import type { Request } from 'express'; + +import { DRIVE_OWNER_COOKIE } from '../google-auth.constants'; +import type { GoogleAuthService } from '../google-auth.service'; +import { DriveOwnerGuard } from './drive-owner.guard'; + +/** + * Builds an ExecutionContext carrying the given parsed cookies. + **/ +const buildContext = (cookies?: Record): ExecutionContext => + ({ + switchToHttp: () => ({ getRequest: () => ({ cookies }) as Partial }), + }) as unknown as ExecutionContext; + +describe('DriveOwnerGuard', () => { + let googleAuthService: { isVerifiedOwner: jest.Mock }; + let guard: DriveOwnerGuard; + + beforeEach(() => { + googleAuthService = { isVerifiedOwner: jest.fn().mockResolvedValue(false) }; + guard = new DriveOwnerGuard(googleAuthService as unknown as GoogleAuthService); + }); + + it('allows the request when the cookie proves ownership', async () => { + googleAuthService.isVerifiedOwner.mockResolvedValue(true); + + await expect(guard.canActivate(buildContext({ [DRIVE_OWNER_COOKIE]: 'proof' }))).resolves.toBe( + true, + ); + expect(googleAuthService.isVerifiedOwner).toHaveBeenCalledWith('proof'); + }); + + it('rejects when the token does not verify (403)', async () => { + googleAuthService.isVerifiedOwner.mockResolvedValue(false); + + await expect( + guard.canActivate(buildContext({ [DRIVE_OWNER_COOKIE]: 'forged' })), + ).rejects.toBeInstanceOf(ForbiddenException); + }); + + it('rejects when the cookie is absent, without inventing a token', async () => { + await expect(guard.canActivate(buildContext({}))).rejects.toBeInstanceOf(ForbiddenException); + expect(googleAuthService.isVerifiedOwner).toHaveBeenCalledWith(undefined); + }); + + it('rejects when no cookies were parsed at all', async () => { + // A missing cookies object must not throw a TypeError — a 500 would hide the authorization decision. + await expect(guard.canActivate(buildContext(undefined))).rejects.toBeInstanceOf( + ForbiddenException, + ); + }); + + it('does not fall back to allowing when the ownership check itself fails', async () => { + googleAuthService.isVerifiedOwner.mockRejectedValue(new Error('db down')); + + await expect( + guard.canActivate(buildContext({ [DRIVE_OWNER_COOKIE]: 'proof' })), + ).rejects.toThrow('db down'); + }); +}); diff --git a/apps/api/src/modules/health/health.controller.ts b/apps/api/src/modules/health/health.controller.ts index 43dd370..284631c 100644 --- a/apps/api/src/modules/health/health.controller.ts +++ b/apps/api/src/modules/health/health.controller.ts @@ -8,7 +8,7 @@ import { } from '@nestjs/terminus'; import { Public } from '@/common/decorators/public.decorator'; -import { SkipRateLimit } from '@/common/services/rate-limit/decorators/skip-rate-limit.decorator'; +import { RateLimit } from '@/common/services/rate-limit/decorators/rate-limit.decorator'; import { PrismaService } from '@/prisma/prisma.service'; const MEMORY_HEAP_THRESHOLD_BYTES = 512 * 1024 * 1024; @@ -16,8 +16,9 @@ const MEMORY_HEAP_THRESHOLD_BYTES = 512 * 1024 * 1024; /** * API health: process heap usage + Postgres reachability. **/ +// Public for the session-less probes, but limited rather than exempt (it reaches Postgres); a 429 reads as unhealthy. @ApiExcludeController() -@SkipRateLimit() +@RateLimit({ points: 120, duration: 60 }) @Controller('health') export class HealthController { constructor( diff --git a/apps/api/src/modules/storage/entities/drive-entry-page.entity.ts b/apps/api/src/modules/storage/entities/drive-entry-page.entity.ts index 1c51279..8354b79 100644 --- a/apps/api/src/modules/storage/entities/drive-entry-page.entity.ts +++ b/apps/api/src/modules/storage/entities/drive-entry-page.entity.ts @@ -1,3 +1,4 @@ +import type { DriveEntryPage } from '@dropto/types'; import { ApiProperty } from '@nestjs/swagger'; import { DriveEntryEntity } from './drive-entry.entity'; @@ -5,7 +6,7 @@ import { DriveEntryEntity } from './drive-entry.entity'; /** * One page of folder contents; `nextPageToken` is the cursor for the next page (null on the last). **/ -export class DriveEntryPageEntity { +export class DriveEntryPageEntity implements DriveEntryPage { @ApiProperty({ type: [DriveEntryEntity], description: 'Entries in this page' }) entries: DriveEntryEntity[]; diff --git a/apps/api/src/modules/storage/entities/drive-entry.entity.ts b/apps/api/src/modules/storage/entities/drive-entry.entity.ts index 94973bf..3d86588 100644 --- a/apps/api/src/modules/storage/entities/drive-entry.entity.ts +++ b/apps/api/src/modules/storage/entities/drive-entry.entity.ts @@ -1,6 +1,7 @@ +import type { DriveEntry } from '@dropto/types'; import { ApiProperty } from '@nestjs/swagger'; -export class DriveEntryEntity { +export class DriveEntryEntity implements DriveEntry { @ApiProperty({ type: 'string', description: 'Opaque file/folder id (Drive id or S3 ref)' }) id: string; diff --git a/apps/api/src/modules/storage/entities/resumable-upload-session.entity.ts b/apps/api/src/modules/storage/entities/resumable-upload-session.entity.ts index 8169fc5..a71000b 100644 --- a/apps/api/src/modules/storage/entities/resumable-upload-session.entity.ts +++ b/apps/api/src/modules/storage/entities/resumable-upload-session.entity.ts @@ -1,9 +1,10 @@ +import type { ResumableUploadSession } from '@dropto/types'; import { ApiProperty } from '@nestjs/swagger'; /** * A resumable upload session; the browser PUTs bytes straight to `uploadUrl`, then calls finalize. **/ -export class ResumableUploadSessionEntity { +export class ResumableUploadSessionEntity implements ResumableUploadSession { @ApiProperty({ type: 'string', description: 'Session URL the browser streams the file bytes to' }) uploadUrl: string; diff --git a/apps/api/src/modules/storage/entities/storage-status.entity.ts b/apps/api/src/modules/storage/entities/storage-status.entity.ts index 4d38794..eff4ff4 100644 --- a/apps/api/src/modules/storage/entities/storage-status.entity.ts +++ b/apps/api/src/modules/storage/entities/storage-status.entity.ts @@ -1,3 +1,4 @@ +import type { StorageRoot, StorageStatus } from '@dropto/types'; import { ApiProperty } from '@nestjs/swagger'; import { StorageBackend } from '../interfaces/storage-provider.interface'; @@ -5,7 +6,7 @@ import { StorageBackend } from '../interfaces/storage-provider.interface'; /** * A browse root within a backend: a Drive folder or an S3 bucket. **/ -export class StorageRootEntity { +export class StorageRootEntity implements StorageRoot { @ApiProperty({ type: 'string', description: 'Opaque id used to browse into this root' }) id: string; @@ -13,7 +14,7 @@ export class StorageRootEntity { name: string; } -export class StorageStatusEntity { +export class StorageStatusEntity implements StorageStatus { @ApiProperty({ enum: ['drive', 's3'], description: 'Storage backend key' }) backend: StorageBackend; diff --git a/apps/api/src/modules/storage/entities/upload-result.entity.ts b/apps/api/src/modules/storage/entities/upload-result.entity.ts index 62ce5a4..5665afd 100644 --- a/apps/api/src/modules/storage/entities/upload-result.entity.ts +++ b/apps/api/src/modules/storage/entities/upload-result.entity.ts @@ -1,6 +1,7 @@ +import type { UploadResult } from '@dropto/types'; import { ApiProperty } from '@nestjs/swagger'; -export class UploadResultEntity { +export class UploadResultEntity implements UploadResult { @ApiProperty({ type: 'string', description: 'Opaque id of the uploaded file (Drive id or S3 ref)', diff --git a/apps/api/src/modules/storage/entities/upload-status.entity.ts b/apps/api/src/modules/storage/entities/upload-status.entity.ts index 3b849d6..5c4688f 100644 --- a/apps/api/src/modules/storage/entities/upload-status.entity.ts +++ b/apps/api/src/modules/storage/entities/upload-status.entity.ts @@ -1,9 +1,10 @@ +import type { ResumableUploadStatus } from '@dropto/types'; import { ApiProperty } from '@nestjs/swagger'; /** * How far a resumable upload session got — lets the browser resume a dropped upload from where it left off **/ -export class UploadStatusEntity { +export class UploadStatusEntity implements ResumableUploadStatus { @ApiProperty({ type: 'boolean', description: 'Whether the upload already finished' }) complete: boolean; diff --git a/apps/api/src/modules/storage/interfaces/storage-provider.interface.ts b/apps/api/src/modules/storage/interfaces/storage-provider.interface.ts index 6663ab2..de1e6b1 100644 --- a/apps/api/src/modules/storage/interfaces/storage-provider.interface.ts +++ b/apps/api/src/modules/storage/interfaces/storage-provider.interface.ts @@ -1,3 +1,4 @@ +import type { StorageBackend } from '@dropto/types'; import { Archiver } from 'archiver'; import { Readable } from 'node:stream'; @@ -9,8 +10,8 @@ import { StorageStatusEntity } from '../entities/storage-status.entity'; import { UploadResultEntity } from '../entities/upload-result.entity'; import { UploadStatusEntity } from '../entities/upload-status.entity'; -/** The storage backends the workspace can browse. */ -export type StorageBackend = 'drive' | 's3'; +// Part of the wire contract, so it lives in @dropto/types; re-exported here as the single import site. +export type { StorageBackend }; /** All known backend keys, in the order they appear in the sidebar. */ export const STORAGE_BACKENDS: StorageBackend[] = ['drive', 's3']; diff --git a/apps/api/src/modules/storage/providers/google-drive.provider.spec.ts b/apps/api/src/modules/storage/providers/google-drive.provider.spec.ts index 8ef32d1..d531ca8 100644 --- a/apps/api/src/modules/storage/providers/google-drive.provider.spec.ts +++ b/apps/api/src/modules/storage/providers/google-drive.provider.spec.ts @@ -28,11 +28,12 @@ describe('GoogleDriveProvider', () => { list: jest.Mock; create: jest.Mock; delete: jest.Mock; + update: jest.Mock; }; + let about: { get: jest.Mock }; let googleAuth: { getActiveAccountId: jest.Mock; getAuthorizedClient: jest.Mock }; let prisma: { - allowedFolder: { findMany: jest.Mock }; - uploadLog: { create: jest.Mock }; + allowedFolder: { findMany: jest.Mock; deleteMany: jest.Mock }; }; /** @@ -46,8 +47,15 @@ describe('GoogleDriveProvider', () => { }); beforeEach(() => { - files = { get: jest.fn(), list: jest.fn(), create: jest.fn(), delete: jest.fn() }; - (google.drive as jest.Mock).mockReturnValue({ files }); + files = { + get: jest.fn(), + list: jest.fn(), + create: jest.fn(), + delete: jest.fn(), + update: jest.fn(), + }; + about = { get: jest.fn() }; + (google.drive as jest.Mock).mockReturnValue({ files, about }); googleAuth = { getActiveAccountId: jest.fn().mockResolvedValue('account-1'), @@ -56,8 +64,7 @@ describe('GoogleDriveProvider', () => { }), }; prisma = { - allowedFolder: { findMany: jest.fn() }, - uploadLog: { create: jest.fn().mockResolvedValue(undefined) }, + allowedFolder: { findMany: jest.fn(), deleteMany: jest.fn().mockResolvedValue({ count: 0 }) }, }; service = new GoogleDriveProvider( @@ -246,14 +253,9 @@ describe('GoogleDriveProvider', () => { size: 1024, webViewLink: 'http://view', }); - expect(prisma.uploadLog.create).toHaveBeenCalledWith( - expect.objectContaining({ - data: expect.objectContaining({ status: 'SUCCESS', fileId: 'up-1' }), - }), - ); }); - it('logs a FAILED entry and rethrows when the Drive upload fails', async () => { + it('rethrows when the Drive upload fails', async () => { withAllowedRoots('root-1'); files.create.mockRejectedValue(new Error('quota exceeded')); @@ -264,46 +266,61 @@ describe('GoogleDriveProvider', () => { mimeType: 'image/jpeg', }), ).rejects.toThrow('quota exceeded'); - - expect(prisma.uploadLog.create).toHaveBeenCalledWith( - expect.objectContaining({ - data: expect.objectContaining({ status: 'FAILED', error: 'quota exceeded' }), - }), - ); }); }); describe('resolveNames', () => { - it('resolves each id to its name + Drive link with a single parallel files.get (no ancestor walk)', async () => { - files.get.mockImplementation(({ fileId }: { fileId: string }) => - Promise.resolve({ - data: { id: fileId, name: `name-${fileId}`, webViewLink: `http://view/${fileId}` }, - }), + it('resolves each authorized id to its name + Drive link', async () => { + withAllowedRoots('a', 'b'); + files.get.mockImplementation(({ fileId, fields }: { fileId: string; fields: string }) => + Promise.resolve( + fields === 'id, parents, trashed' + ? { data: { id: fileId, parents: [] } } + : { + data: { id: fileId, name: `name-${fileId}`, webViewLink: `http://view/${fileId}` }, + }, + ), ); - const result = await service.resolveNames(['a', 'b']); - - expect(result).toEqual([ + await expect(service.resolveNames(['a', 'b'])).resolves.toEqual([ { id: 'a', name: 'name-a', webViewLink: 'http://view/a' }, { id: 'b', name: 'name-b', webViewLink: 'http://view/b' }, ]); - // One lookup per id, fetching only id + name + link — never the parents walk. - expect(files.get).toHaveBeenCalledTimes(2); expect(files.get).toHaveBeenCalledWith({ fileId: 'a', fields: 'id, name, webViewLink' }); }); it('falls back to an empty name / null link for ids the app cannot see', async () => { - files.get - .mockResolvedValueOnce({ data: { id: 'ok', name: 'Visible', webViewLink: 'http://view' } }) - .mockRejectedValueOnce(new Error('404 Not Found')); - - const result = await service.resolveNames(['ok', 'hidden']); + withAllowedRoots('ok'); + files.get.mockImplementation(({ fileId, fields }: { fileId: string; fields: string }) => { + if (fields === 'id, parents, trashed') { + return Promise.resolve({ data: { id: fileId, parents: [] } }); + } + return fileId === 'ok' + ? Promise.resolve({ data: { id: 'ok', name: 'Visible', webViewLink: 'http://view' } }) + : Promise.reject(new Error('404 Not Found')); + }); - expect(result).toEqual([ + await expect(service.resolveNames(['ok', 'hidden'])).resolves.toEqual([ { id: 'ok', name: 'Visible', webViewLink: 'http://view' }, { id: 'hidden', name: '', webViewLink: null }, ]); }); + + it('does not leak the name of a file outside the authorized tree', async () => { + withAllowedRoots('root-1'); + // The id resolves in Drive (the grant covers the whole account) but hangs off another tree. + files.get.mockImplementation(({ fileId, fields }: { fileId: string; fields: string }) => + Promise.resolve( + fields === 'id, parents, trashed' + ? { data: { id: fileId, parents: ['somewhere-else'] } } + : { data: { id: fileId, name: 'Tax return 2025.pdf', webViewLink: 'http://view' } }, + ), + ); + + await expect(service.resolveNames(['outsider'])).resolves.toEqual([ + { id: 'outsider', name: '', webViewLink: null }, + ]); + }); }); describe('createFolderArchive', () => { @@ -313,7 +330,6 @@ describe('GoogleDriveProvider', () => { await expect(service.createFolderArchive('root-1')).rejects.toBeInstanceOf( BadRequestException, ); - // Rejected before any metadata lookup or archive stream is opened. expect(files.get).not.toHaveBeenCalled(); }); @@ -327,6 +343,414 @@ describe('GoogleDriveProvider', () => { }); }); + describe('listRoots (liveness pruning)', () => { + const row = { id: 'f1', folderId: 'drive-1', name: 'Photos', createdAt: new Date() }; + // The rows are mapped to the wire entity on the way out, which serializes `createdAt`. + const entity = { ...row, createdAt: row.createdAt.toISOString() }; + + /** + * Scripts the authorized-root rows independently of `getAllowedFolderIds`. + **/ + const withRootRows = () => { + prisma.allowedFolder.findMany.mockResolvedValue([row]); + }; + + it('returns a root that still exists in Drive', async () => { + withRootRows(); + files.get.mockResolvedValue({ data: { id: 'drive-1', trashed: false } }); + + await expect(service.listRoots()).resolves.toEqual([entity]); + expect(prisma.allowedFolder.deleteMany).not.toHaveBeenCalled(); + }); + + it('hides a trashed root but keeps the authorization (it can be restored in Drive)', async () => { + withRootRows(); + files.get.mockResolvedValue({ data: { id: 'drive-1', trashed: true } }); + + await expect(service.listRoots()).resolves.toEqual([]); + expect(prisma.allowedFolder.deleteMany).not.toHaveBeenCalled(); + }); + + it('drops and unauthorizes a root Drive reports as gone (404)', async () => { + withRootRows(); + files.get.mockRejectedValue(Object.assign(new Error('File not found'), { code: 404 })); + + await expect(service.listRoots()).resolves.toEqual([]); + expect(prisma.allowedFolder.deleteMany).toHaveBeenCalledWith({ + where: { driveAccountId: 'account-1', id: { in: ['f1'] } }, + }); + }); + + it('keeps a root when the liveness check fails transiently', async () => { + withRootRows(); + // Only a hard 404 is proof; treating a 500 as proof would revoke a working authorization. + files.get.mockRejectedValue(Object.assign(new Error('backend error'), { code: 500 })); + + await expect(service.listRoots()).resolves.toEqual([entity]); + expect(prisma.allowedFolder.deleteMany).not.toHaveBeenCalled(); + }); + }); + + describe('renameItem', () => { + it('renames an item inside the authorized tree', async () => { + withAllowedRoots('root-1'); + files.get.mockResolvedValue({ data: { id: 'doc', parents: ['root-1'] } }); + files.update.mockResolvedValue({ + data: { id: 'doc', name: 'new.pdf', mimeType: 'application/pdf', size: '10' }, + }); + + await expect(service.renameItem('doc', 'new.pdf')).resolves.toMatchObject({ + id: 'doc', + name: 'new.pdf', + isFolder: false, + size: 10, + }); + expect(files.update).toHaveBeenCalledWith( + expect.objectContaining({ fileId: 'doc', requestBody: { name: 'new.pdf' } }), + ); + }); + + it('refuses to rename an authorized root (409)', async () => { + withAllowedRoots('root-1'); + + await expect(service.renameItem('root-1', 'Renamed')).rejects.toBeInstanceOf( + ConflictException, + ); + expect(files.update).not.toHaveBeenCalled(); + }); + + it('refuses to rename an item outside the authorized tree (403)', async () => { + withAllowedRoots('root-1'); + files.get.mockResolvedValue({ data: { id: 'outsider', parents: [] } }); + + await expect(service.renameItem('outsider', 'new')).rejects.toBeInstanceOf( + ForbiddenException, + ); + expect(files.update).not.toHaveBeenCalled(); + }); + }); + + describe('moveItem', () => { + it('reparents an item, detaching it from its previous parents', async () => { + withAllowedRoots('root-1'); + files.get.mockImplementation(({ fileId }: { fileId: string }) => + Promise.resolve({ data: { id: fileId, parents: ['root-1'] } }), + ); + files.update.mockResolvedValue({ + data: { id: 'doc', name: 'doc.pdf', mimeType: 'application/pdf' }, + }); + + await expect(service.moveItem('doc', 'sub-1')).resolves.toMatchObject({ id: 'doc' }); + expect(files.update).toHaveBeenCalledWith( + expect.objectContaining({ + fileId: 'doc', + addParents: 'sub-1', + removeParents: 'root-1', + }), + ); + }); + + it('rejects moving an item into itself (400)', async () => { + withAllowedRoots('root-1'); + + await expect(service.moveItem('doc', 'doc')).rejects.toBeInstanceOf(BadRequestException); + expect(files.update).not.toHaveBeenCalled(); + }); + + it('refuses to move an authorized root (409)', async () => { + withAllowedRoots('root-1'); + + await expect(service.moveItem('root-1', 'sub-1')).rejects.toBeInstanceOf(ConflictException); + expect(files.update).not.toHaveBeenCalled(); + }); + + it('validates the destination too, not just the item (403)', async () => { + withAllowedRoots('root-1'); + // Checking only the source would let an operator move an authorized file out into the rest of the Drive. + files.get.mockImplementation(({ fileId }: { fileId: string }) => + Promise.resolve({ + data: { id: fileId, parents: fileId === 'doc' ? ['root-1'] : [] }, + }), + ); + + await expect(service.moveItem('doc', 'elsewhere')).rejects.toBeInstanceOf(ForbiddenException); + expect(files.update).not.toHaveBeenCalled(); + }); + }); + + describe('downloadFile', () => { + it('returns the stream with the file metadata', async () => { + withAllowedRoots('root-1'); + const stream = { pipe: jest.fn() }; + files.get.mockImplementation(({ alt, fields }: { alt?: string; fields?: string }) => { + if (alt === 'media') return Promise.resolve({ data: stream }); + if (fields === 'name, mimeType, size') { + return Promise.resolve({ + data: { name: 'doc.pdf', mimeType: 'application/pdf', size: '2048' }, + }); + } + return Promise.resolve({ data: { id: 'doc', parents: ['root-1'] } }); + }); + + await expect(service.downloadFile('doc')).resolves.toMatchObject({ + stream, + name: 'doc.pdf', + mimeType: 'application/pdf', + size: 2048, + }); + }); + + it('refuses to stream a folder as a file (400)', async () => { + withAllowedRoots('root-1'); + files.get.mockImplementation(({ fields }: { fields?: string }) => + Promise.resolve( + fields === 'name, mimeType, size' + ? { data: { name: 'Sub', mimeType: FOLDER_MIME } } + : { data: { id: 'sub', parents: ['root-1'] } }, + ), + ); + + await expect(service.downloadFile('sub')).rejects.toBeInstanceOf(BadRequestException); + }); + + it('refuses a file outside the authorized tree (403)', async () => { + withAllowedRoots('root-1'); + files.get.mockResolvedValue({ data: { id: 'outsider', parents: [] } }); + + await expect(service.downloadFile('outsider')).rejects.toBeInstanceOf(ForbiddenException); + }); + }); + + // The only paths reaching Google over raw `fetch`; test/setup-env.ts makes an unstubbed call throw. + describe('createResumableUpload', () => { + const init = { + name: 'big.bin', + mimeType: 'application/octet-stream', + size: 1024, + origin: 'http://localhost:3000', + }; + let fetchMock: jest.Mock; + + beforeEach(() => { + fetchMock = jest.fn(); + global.fetch = fetchMock as unknown as typeof fetch; + }); + + it('returns the session URL Google puts in the Location header', async () => { + withAllowedRoots('root-1'); + fetchMock.mockResolvedValue({ + ok: true, + status: 200, + headers: new Headers({ + location: 'https://www.googleapis.com/upload/drive/v3/files?upload_id=X', + }), + }); + + await expect(service.createResumableUpload('root-1', init)).resolves.toEqual({ + uploadUrl: 'https://www.googleapis.com/upload/drive/v3/files?upload_id=X', + }); + }); + + it('bakes the validated parent and the browser origin into the session', async () => { + withAllowedRoots('root-1'); + fetchMock.mockResolvedValue({ + ok: true, + status: 200, + headers: new Headers({ location: 'https://www.googleapis.com/upload/drive/session' }), + }); + + await service.createResumableUpload('root-1', init); + + const [, options] = fetchMock.mock.calls[0] as [string, RequestInit]; + // The parent is server-side only: the browser gets a session it cannot repoint elsewhere. + expect(JSON.parse(options.body as string)).toEqual({ + name: 'big.bin', + parents: ['root-1'], + mimeType: 'application/octet-stream', + }); + expect((options.headers as Record)['Origin']).toBe('http://localhost:3000'); + }); + + it('refuses to open a session for a folder outside the authorized tree (403)', async () => { + withAllowedRoots('root-1'); + files.get.mockResolvedValue({ data: { id: 'outsider', parents: [] } }); + + await expect(service.createResumableUpload('outsider', init)).rejects.toBeInstanceOf( + ForbiddenException, + ); + expect(fetchMock).not.toHaveBeenCalled(); + }); + + it('maps a rejected session to a 503', async () => { + withAllowedRoots('root-1'); + fetchMock.mockResolvedValue({ ok: false, status: 403, headers: new Headers() }); + + await expect(service.createResumableUpload('root-1', init)).rejects.toBeInstanceOf( + ServiceUnavailableException, + ); + }); + + it('maps a 200 without a Location header to a 503', async () => { + withAllowedRoots('root-1'); + fetchMock.mockResolvedValue({ ok: true, status: 200, headers: new Headers() }); + + await expect(service.createResumableUpload('root-1', init)).rejects.toBeInstanceOf( + ServiceUnavailableException, + ); + }); + }); + + describe('getUploadStatus', () => { + let fetchMock: jest.Mock; + + beforeEach(() => { + fetchMock = jest.fn(); + global.fetch = fetchMock as unknown as typeof fetch; + }); + + // The URL is request-supplied, so this prefix check is all that stops an SSRF into the internal network. + it.each([ + ['an unrelated host', 'https://evil.example.com/upload/drive/v3/files'], + ['plain http', 'http://www.googleapis.com/upload/drive/v3/files'], + ['an internal address', 'http://169.254.169.254/latest/meta-data/'], + ['a look-alike host', 'https://www.googleapis.com.evil.test/upload/drive/'], + [ + 'the prefix buried in a query string', + 'https://evil.test/?u=https://www.googleapis.com/upload/drive/', + ], + ])('rejects %s without dialling it (400)', async (_label, url) => { + await expect(service.getUploadStatus(url, 100)).rejects.toBeInstanceOf(BadRequestException); + expect(fetchMock).not.toHaveBeenCalled(); + }); + + it('reports a finished session with the created file id', async () => { + fetchMock.mockResolvedValue({ + status: 200, + headers: new Headers(), + json: jest.fn().mockResolvedValue({ id: 'file-1' }), + }); + + await expect( + service.getUploadStatus( + 'https://www.googleapis.com/upload/drive/v3/files?upload_id=X', + 500, + ), + ).resolves.toEqual({ complete: true, receivedBytes: 500, fileId: 'file-1' }); + }); + + it('survives a 200 whose body is not JSON', async () => { + fetchMock.mockResolvedValue({ + status: 201, + headers: new Headers(), + json: jest.fn().mockRejectedValue(new Error('not json')), + }); + + await expect( + service.getUploadStatus('https://www.googleapis.com/upload/drive/v3/files', 500), + ).resolves.toEqual({ complete: true, receivedBytes: 500, fileId: null }); + }); + + it('translates a 308 Range header into the received byte count', async () => { + // "bytes=0-99" means bytes 0..99 inclusive landed, i.e. 100 bytes. + fetchMock.mockResolvedValue({ + status: 308, + headers: new Headers({ range: 'bytes=0-99' }), + }); + + await expect( + service.getUploadStatus('https://www.googleapis.com/upload/drive/v3/files', 500), + ).resolves.toEqual({ complete: false, receivedBytes: 100, fileId: null }); + }); + + it('treats a 308 without a Range header as nothing received', async () => { + fetchMock.mockResolvedValue({ status: 308, headers: new Headers() }); + + await expect( + service.getUploadStatus('https://www.googleapis.com/upload/drive/v3/files', 500), + ).resolves.toEqual({ complete: false, receivedBytes: 0, fileId: null }); + }); + + it('falls back to 0 when the Range header is unparseable', async () => { + fetchMock.mockResolvedValue({ + status: 308, + headers: new Headers({ range: 'bytes=garbage' }), + }); + + await expect( + service.getUploadStatus('https://www.googleapis.com/upload/drive/v3/files', 500), + ).resolves.toMatchObject({ receivedBytes: 0 }); + }); + + it('maps any other status to a 503', async () => { + fetchMock.mockResolvedValue({ status: 500, headers: new Headers() }); + + await expect( + service.getUploadStatus('https://www.googleapis.com/upload/drive/v3/files', 500), + ).rejects.toBeInstanceOf(ServiceUnavailableException); + }); + }); + + describe('status (quota caching)', () => { + /** + * A connected account with one authorized root that Drive still reports as live. + **/ + const connected = () => { + googleAuth.getStatus = jest.fn().mockResolvedValue({ + connected: true, + email: 'owner@gmail.com', + allowedFolders: [{ id: 'f1', folderId: 'root-1', name: 'test', createdAt: new Date() }], + }); + about.get.mockResolvedValue({ data: { storageQuota: { usage: '100', limit: '1000' } } }); + files.get.mockResolvedValue({ data: { id: 'root-1', trashed: false } }); + }; + + it('reports the quota from Drive on the first call', async () => { + connected(); + + await expect(service.status()).resolves.toMatchObject({ + connected: true, + quota: { usage: 100, limit: 1000 }, + }); + expect(about.get).toHaveBeenCalledTimes(1); + }); + + it('serves the quota from cache on the next call', async () => { + connected(); + + await service.status(); + await service.status(); + await service.status(); + + // One `about.get` for three polls: the quota is what the cache is for. + expect(about.get).toHaveBeenCalledTimes(1); + }); + + it('still re-checks root liveness on every call', async () => { + connected(); + + await service.status(); + const afterFirst = files.get.mock.calls.length; + await service.status(); + + // Deliberately uncached: a root deleted in Drive has to disappear from the sidebar at once. + expect(files.get.mock.calls.length).toBeGreaterThan(afterFirst); + }); + + it('re-reads the quota once the TTL has passed', async () => { + connected(); + const now = Date.now(); + const clock = jest.spyOn(Date, 'now'); + + clock.mockReturnValue(now); + await service.status(); + clock.mockReturnValue(now + 30_001); + await service.status(); + + expect(about.get).toHaveBeenCalledTimes(2); + clock.mockRestore(); + }); + }); + describe('getDrive (token refresh guard)', () => { it('maps an invalid_grant (revoked token) to a 424 StorageDisconnectedException', async () => { googleAuth.getAuthorizedClient.mockResolvedValue({ diff --git a/apps/api/src/modules/storage/providers/google-drive.provider.ts b/apps/api/src/modules/storage/providers/google-drive.provider.ts index 296a76a..2c21568 100644 --- a/apps/api/src/modules/storage/providers/google-drive.provider.ts +++ b/apps/api/src/modules/storage/providers/google-drive.provider.ts @@ -12,7 +12,11 @@ import { Archiver, ZipArchive } from 'archiver'; import { Auth, drive_v3, google } from 'googleapis'; import { Readable } from 'node:stream'; -import { AllowedFolderEntity } from '@/modules/google-auth/entities/allowed-folder.entity'; +import { TtlCache } from '@/common/utils/ttl-cache.functions'; +import { + AllowedFolderEntity, + toAllowedFolderEntity, +} from '@/modules/google-auth/entities/allowed-folder.entity'; import { GoogleAuthService } from '@/modules/google-auth/google-auth.service'; import { PrismaService } from '@/prisma/prisma.service'; @@ -38,7 +42,6 @@ import { StorageDisconnectedException, } from '../storage.errors'; import { finalizeArchiveInBackground, sanitizeZipEntryPath } from '../storage.functions'; -import { logUploadFailure, logUploadSuccess } from '../upload-log.functions'; const FOLDER_MIME = 'application/vnd.google-apps.folder'; const MAX_ANCESTOR_DEPTH = 50; @@ -48,6 +51,15 @@ const ITEM_MISSING_MESSAGE = 'This item no longer exists in Google Drive.'; // One network page. Small enough to keep first paint fast, big enough that most folders need one call. const PAGE_SIZE = 100; +/** How long the reported storage quota stays trusted. */ +// Only the quota is cached; root liveness must stay live (an e2e caught a cached deleted root lingering). +const QUOTA_CACHE_TTL_MS = 30_000; + +/** How long a folder's parent list stays trusted. Ancestry changes only when a folder is moved. */ +const ANCESTOR_CACHE_TTL_MS = 60_000; + +const ANCESTOR_CACHE_MAX = 500; + /** * StorageProvider over the Drive API; every op is validated to stay inside the authorized folder tree. **/ @@ -55,6 +67,16 @@ const PAGE_SIZE = 100; export class GoogleDriveProvider implements StorageProvider { readonly backend: StorageBackend = 'drive'; + // Intermediate folders only — the target item is always re-read, so a deleted item still 404s. + private readonly ancestorCache = new TtlCache( + ANCESTOR_CACHE_TTL_MS, + ANCESTOR_CACHE_MAX, + ); + private quotaCache: { + value: { usage: number; limit: number | null } | null; + expiresAt: number; + } | null = null; + private readonly logger = new Logger(GoogleDriveProvider.name); constructor( @@ -76,7 +98,7 @@ export class GoogleDriveProvider implements StorageProvider { const accountId = await this.googleAuthService.getActiveAccountId(); const auth = await this.googleAuthService.getAuthorizedClient(accountId); await auth.getAccessToken(); - quota = await this.fetchQuota(auth); + quota = await this.cachedQuota(auth); // The owner can delete an authorized folder in Drive; don't offer it as a browse root. folders = await this.pruneMissingRoots( google.drive({ version: 'v3', auth }), @@ -107,6 +129,24 @@ export class GoogleDriveProvider implements StorageProvider { }; } + /** + * The storage quota, re-read from Drive at most once per TTL. + **/ + private async cachedQuota( + auth: Auth.OAuth2Client, + ): Promise<{ usage: number; limit: number | null } | null> { + const now = Date.now(); + + if (this.quotaCache && this.quotaCache.expiresAt > now) { + return this.quotaCache.value; + } + + const value = await this.fetchQuota(auth); + this.quotaCache = { value, expiresAt: now + QUOTA_CACHE_TTL_MS }; + + return value; + } + /** * The connected account's storage usage (`about.get`); best-effort — null if it can't be read. **/ @@ -162,8 +202,7 @@ export class GoogleDriveProvider implements StorageProvider { } /** - * Reads the target's parents. Gone in Drive (deleted/trashed) → 404; unreadable for any other - * reason → null, letting the ancestor walk end in the usual 403. + * Reads the target's parents; gone in Drive → 404, unreadable for any other reason → null. **/ private async getItemParents(drive: drive_v3.Drive, itemId: string): Promise { try { @@ -184,9 +223,7 @@ export class GoogleDriveProvider implements StorageProvider { } /** - * Passes if the id is an authorized root or has one as an ancestor; else 403. Authorized roots are - * still verified against Drive — the owner can delete one there, and the stale id would otherwise - * reach the API and blow up as an unhandled 404. + * Passes if the id is an authorized root or has one as an ancestor; else 403 (roots re-verified against Drive). **/ private async assertItemAllowed( drive: drive_v3.Drive, @@ -215,9 +252,19 @@ export class GoogleDriveProvider implements StorageProvider { } visited.add(id); + const cached = this.ancestorCache.get(id); + + if (cached) { + next.push(...cached); + continue; + } + try { const res = await drive.files.get({ fileId: id, fields: 'id, parents' }); - next.push(...(res.data.parents ?? [])); + const parents = res.data.parents ?? []; + + this.ancestorCache.set(id, parents); + next.push(...parents); } catch { // Not reachable (deleted / no access) — treat as not allowed. continue; @@ -232,9 +279,7 @@ export class GoogleDriveProvider implements StorageProvider { } /** - * Drops authorized roots that are gone from Drive so the sidebar never offers a folder every - * later call would 404 on. A hard 404 means gone for good → the authorization is pruned too; - * trashed folders are only hidden (restoring one in Drive brings it back). + * Drops roots gone from Drive: a hard 404 unauthorizes them, a trashed folder is only hidden. **/ private async pruneMissingRoots( drive: drive_v3.Drive, @@ -277,7 +322,8 @@ export class GoogleDriveProvider implements StorageProvider { orderBy: { createdAt: 'asc' }, }); - return this.pruneMissingRoots(drive, driveAccountId, folders); + // Deliberately uncached: reporting which roots are still live is the whole job (an e2e caught a cached one lingering). + return this.pruneMissingRoots(drive, driveAccountId, folders.map(toAllowedFolderEntity)); } async listContents(folderId: string, options: ListContentsOptions = {}): Promise { @@ -333,15 +379,24 @@ export class GoogleDriveProvider implements StorageProvider { async resolveNames(ids: string[]): Promise { const driveAccountId = await this.googleAuthService.getActiveAccountId(); const drive = await this.getDrive(driveAccountId); + const allowedIds = await this.getAllowedFolderIds(driveAccountId); - // Breadcrumb names + Drive links only — one parallel files.get per id, no ancestor walk (data access still validates the full tree). + // Validated first: the grant covers the whole Drive, so an unchecked id would leak any file's name and link. return Promise.all( ids.map(async (id) => { + const empty = { id, name: '', webViewLink: null }; + + try { + await this.assertItemAllowed(drive, id, allowedIds); + } catch { + return empty; + } + try { const res = await drive.files.get({ fileId: id, fields: 'id, name, webViewLink' }); return { id, name: res.data.name ?? '', webViewLink: res.data.webViewLink ?? null }; } catch { - return { id, name: '', webViewLink: null }; + return empty; } }), ); @@ -370,32 +425,25 @@ export class GoogleDriveProvider implements StorageProvider { await this.assertItemAllowed(drive, folderId, allowedIds); - try { - const res = await drive.files.create( - { - requestBody: { name: fileName, parents: [folderId] }, - media: { mimeType, body }, - fields: 'id, name, size, webViewLink', - }, - { signal }, - ); - - const size = this.parseSize(res.data.size); + const res = await drive.files.create( + { + requestBody: { name: fileName, parents: [folderId] }, + media: { mimeType, body }, + fields: 'id, name, size, webViewLink', + }, + { signal }, + ); - await logUploadSuccess(this.prismaService, { fileName, folderId, fileId: res.data.id, size }); + const size = this.parseSize(res.data.size); - this.logger.log(`Uploaded "${fileName}" (${res.data.id}) into ${folderId}.`); + this.logger.log(`Uploaded "${fileName}" (${res.data.id}) into ${folderId}.`); - return { - fileId: res.data.id ?? '', - fileName: res.data.name ?? fileName, - size, - webViewLink: res.data.webViewLink ?? null, - }; - } catch (error) { - await logUploadFailure(this.prismaService, { fileName, folderId, error, signal }); - throw error; - } + return { + fileId: res.data.id ?? '', + fileName: res.data.name ?? fileName, + size, + webViewLink: res.data.webViewLink ?? null, + }; } /** @@ -494,13 +542,6 @@ export class GoogleDriveProvider implements StorageProvider { const size = this.parseSize(res.data.size); const fileName = res.data.name ?? ''; - await logUploadSuccess(this.prismaService, { - fileName, - folderId: res.data.parents?.[0] ?? '', - fileId: res.data.id, - size, - }); - this.logger.log(`Finalized upload "${fileName}" (${res.data.id}).`); return { diff --git a/apps/api/src/modules/storage/providers/s3-storage.provider.spec.ts b/apps/api/src/modules/storage/providers/s3-storage.provider.spec.ts index cf807c6..d3754a3 100644 --- a/apps/api/src/modules/storage/providers/s3-storage.provider.spec.ts +++ b/apps/api/src/modules/storage/providers/s3-storage.provider.spec.ts @@ -9,7 +9,6 @@ import { } from '@nestjs/common'; import type { S3Config } from '@/config/s3.config'; -import type { PrismaService } from '@/prisma/prisma.service'; import { S3_UNAVAILABLE_MESSAGE, StorageDisconnectedException } from '../storage.errors'; import { S3StorageProvider } from './s3-storage.provider'; @@ -50,10 +49,9 @@ const baseCfg: S3Config = { describe('S3StorageProvider', () => { let send: jest.Mock; let uploadDone: jest.Mock; - let prisma: { uploadLog: { create: jest.Mock } }; const make = (override: Partial = {}) => - new S3StorageProvider({ ...baseCfg, ...override }, prisma as unknown as PrismaService); + new S3StorageProvider({ ...baseCfg, ...override }); const sentCmds = () => send.mock.calls.map((call) => call[0] as { cmd: string; input: Record }); @@ -71,7 +69,6 @@ describe('S3StorageProvider', () => { done: uploadDone, abort: jest.fn(), })); - prisma = { uploadLog: { create: jest.fn().mockResolvedValue(undefined) } }; }); describe('guards (resolve / ensureEnabled)', () => { @@ -374,12 +371,9 @@ describe('S3StorageProvider', () => { size: 100, webViewLink: null, }); - expect(prisma.uploadLog.create).toHaveBeenCalledWith( - expect.objectContaining({ data: expect.objectContaining({ status: 'SUCCESS' }) }), - ); }); - it('logs FAILED and rethrows when the upload fails', async () => { + it('rethrows when the upload fails', async () => { uploadDone.mockRejectedValue(new Error('nope')); await expect( @@ -389,12 +383,6 @@ describe('S3StorageProvider', () => { mimeType: 'text/plain', }), ).rejects.toThrow('nope'); - - expect(prisma.uploadLog.create).toHaveBeenCalledWith( - expect.objectContaining({ - data: expect.objectContaining({ status: 'FAILED', error: 'nope' }), - }), - ); }); }); }); diff --git a/apps/api/src/modules/storage/providers/s3-storage.provider.ts b/apps/api/src/modules/storage/providers/s3-storage.provider.ts index f181b5a..d39194d 100644 --- a/apps/api/src/modules/storage/providers/s3-storage.provider.ts +++ b/apps/api/src/modules/storage/providers/s3-storage.provider.ts @@ -24,7 +24,6 @@ import { Readable } from 'node:stream'; import { type S3Config, s3Config } from '@/config/s3.config'; import { AllowedFolderEntity } from '@/modules/google-auth/entities/allowed-folder.entity'; -import { PrismaService } from '@/prisma/prisma.service'; import { DriveEntryEntity } from '../entities/drive-entry.entity'; import { ResolvedNameEntity } from '../entities/resolved-name.entity'; @@ -48,7 +47,6 @@ import { StorageDisconnectedException, } from '../storage.errors'; import { finalizeArchiveInBackground, sanitizeZipEntryPath } from '../storage.functions'; -import { logUploadFailure, logUploadSuccess } from '../upload-log.functions'; /** Marker used for zero-byte "folder" objects (a prefix ending in a slash). */ const FOLDER_SUFFIX = '/'; @@ -146,10 +144,7 @@ export class S3StorageProvider implements StorageProvider { // Buckets already reported as gone, so the warning is logged once per disappearance. private readonly missingBuckets = new Set(); - constructor( - @Inject(s3Config.KEY) private readonly cfg: S3Config, - private readonly prismaService: PrismaService, - ) { + constructor(@Inject(s3Config.KEY) private readonly cfg: S3Config) { this.allowedBuckets = new Set(cfg.buckets); } @@ -218,7 +213,8 @@ export class S3StorageProvider implements StorageProvider { id: encodeId({ bucket, key: '' }), folderId: encodeId({ bucket, key: '' }), name: bucket, - createdAt: new Date(0), + // Buckets carry no creation date of their own; the epoch keeps the shape uniform. + createdAt: new Date(0).toISOString(), })); } @@ -309,8 +305,6 @@ export class S3StorageProvider implements StorageProvider { const size = await this.headSize(ref.bucket, key); - await logUploadSuccess(this.prismaService, { fileName, folderId, fileId: key, size }); - this.logger.log(`Uploaded "${fileName}" into ${ref.bucket}/${key}.`); return { @@ -320,7 +314,6 @@ export class S3StorageProvider implements StorageProvider { webViewLink: null, }; } catch (error) { - await logUploadFailure(this.prismaService, { fileName, folderId, error, signal }); throw this.toHttpError(error); } finally { signal?.removeEventListener('abort', onAbort); @@ -510,9 +503,7 @@ export class S3StorageProvider implements StorageProvider { // --- Internals ---------------------------------------------------------------- /** - * Probes every configured bucket in parallel and returns the ones that answered. A bucket that - * was deleted outside the app is dropped (never offered as a browse root); any other failure - * (bad credentials, unreachable endpoint, 5xx) is a whole-backend problem and propagates. + * Probes every configured bucket in parallel; a deleted one is dropped, any other failure propagates. **/ private async liveBuckets(): Promise { const client = this.getClient(); @@ -542,8 +533,7 @@ export class S3StorageProvider implements StorageProvider { } /** - * Turns an AWS error into a clean HTTP one: a deleted bucket/object → 404, a whole-backend - * failure → 424. The cached status is dropped so the next poll re-probes the buckets. + * Turns an AWS error into a clean HTTP one: deleted bucket/object → 404, whole-backend failure → 424. **/ private toHttpError(error: unknown): never { if (isNotFoundError(error)) { diff --git a/apps/api/src/modules/storage/storage.controller.ts b/apps/api/src/modules/storage/storage.controller.ts index 8abc70c..94e2f33 100644 --- a/apps/api/src/modules/storage/storage.controller.ts +++ b/apps/api/src/modules/storage/storage.controller.ts @@ -53,6 +53,12 @@ import { UploadStatusEntity } from './entities/upload-status.entity'; import { sanitizeUploadFilename } from './storage.functions'; import { StorageRegistry } from './storage.registry'; +/** + * Cap on `GET :backend/names?ids=` — each id costs an authorization walk plus a metadata read. + **/ +// Never binds in practice; it exists so a crafted `ids=` cannot fan out into unbounded upstream calls. +const MAX_RESOLVE_IDS = 50; + /** * Documents the `:backend` path param (drive | s3) on the routes that carry it. **/ @@ -114,16 +120,18 @@ export class StorageController { @ApiOperation({ summary: 'Resolve display names for a set of ids (breadcrumb rebuild)' }) @ApiOkResponse({ type: [ResolvedNameEntity], description: 'Id → name pairs' }) + @ApiBadRequestResponse({ type: ResponseEntity, description: 'Too many ids' }) @BackendParam() @Get(':backend/names') async resolveNames( @Param('backend') backend: string, @Query('ids') ids?: string, ): Promise { - const list = (ids ?? '') - .split(',') - .map((id) => id.trim()) - .filter(Boolean); + const list = [...new Set((ids ?? '').split(',').map((id) => id.trim()).filter(Boolean))]; + + if (list.length > MAX_RESOLVE_IDS) { + throw new BadRequestException(`At most ${MAX_RESOLVE_IDS} ids can be resolved per request.`); + } return list.length === 0 ? [] : this.registry.resolve(backend).resolveNames(list); } diff --git a/apps/api/src/modules/storage/upload-log.functions.ts b/apps/api/src/modules/storage/upload-log.functions.ts deleted file mode 100644 index 3f3d3c2..0000000 --- a/apps/api/src/modules/storage/upload-log.functions.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { PrismaService } from '@/prisma/prisma.service'; - -interface UploadLogSuccess { - fileName: string; - folderId: string; - fileId: string | null | undefined; - size: number | null; -} - -interface UploadLogFailure { - fileName: string; - folderId: string; - error: unknown; - signal?: AbortSignal; -} - -/** - * Records a successful upload in the audit log (shared by every storage provider). - **/ -export const logUploadSuccess = async ( - prisma: PrismaService, - entry: UploadLogSuccess, -): Promise => { - await prisma.uploadLog.create({ - data: { - fileName: entry.fileName, - folderId: entry.folderId, - fileId: entry.fileId ?? null, - size: entry.size === null ? null : BigInt(entry.size), - status: 'SUCCESS', - }, - }); -}; - -/** - * Records a failed upload — but not a client-cancelled one, which isn't a real failure. - **/ -export const logUploadFailure = async ( - prisma: PrismaService, - entry: UploadLogFailure, -): Promise => { - if (entry.signal?.aborted) { - return; - } - - await prisma.uploadLog.create({ - data: { - fileName: entry.fileName, - folderId: entry.folderId, - status: 'FAILED', - error: entry.error instanceof Error ? entry.error.message : String(entry.error), - }, - }); -}; diff --git a/apps/api/src/prisma/dbml/schema.dbml b/apps/api/src/prisma/dbml/schema.dbml index e039d71..5bd4883 100644 --- a/apps/api/src/prisma/dbml/schema.dbml +++ b/apps/api/src/prisma/dbml/schema.dbml @@ -58,22 +58,4 @@ rotations so a session can\'t outlive `login + absolute TTL`. `subject` keys row the table is ready for multiple users. Expired rows are pruned on refresh to keep it small.' } -Table upload_logs { - id String [pk] - fileName String [not null] - folderId String [not null] - fileId String - size BigInt - status UploadStatus [not null] - error String - createdAt DateTime [default: `now()`, not null] - - Note: 'Audit trail of upload attempts for the history view.' -} - -Enum UploadStatus { - SUCCESS - FAILED -} - Ref: allowed_folders.driveAccountId > drive_accounts.id [delete: Cascade] \ No newline at end of file diff --git a/apps/api/src/prisma/generated/prisma/browser.ts b/apps/api/src/prisma/generated/prisma/browser.ts index 6c4ccc2..6ea802b 100644 --- a/apps/api/src/prisma/generated/prisma/browser.ts +++ b/apps/api/src/prisma/generated/prisma/browser.ts @@ -46,8 +46,3 @@ export type AllowedFolder = Prisma.AllowedFolderModel * the table is ready for multiple users. Expired rows are pruned on refresh to keep it small. */ export type RefreshToken = Prisma.RefreshTokenModel -/** - * Model UploadLog - * Audit trail of upload attempts for the history view. - */ -export type UploadLog = Prisma.UploadLogModel diff --git a/apps/api/src/prisma/generated/prisma/client.ts b/apps/api/src/prisma/generated/prisma/client.ts index fa03000..d120c01 100644 --- a/apps/api/src/prisma/generated/prisma/client.ts +++ b/apps/api/src/prisma/generated/prisma/client.ts @@ -68,8 +68,3 @@ export type AllowedFolder = Prisma.AllowedFolderModel * the table is ready for multiple users. Expired rows are pruned on refresh to keep it small. */ export type RefreshToken = Prisma.RefreshTokenModel -/** - * Model UploadLog - * Audit trail of upload attempts for the history view. - */ -export type UploadLog = Prisma.UploadLogModel diff --git a/apps/api/src/prisma/generated/prisma/commonInputTypes.ts b/apps/api/src/prisma/generated/prisma/commonInputTypes.ts index ea9b4a7..fbdee81 100644 --- a/apps/api/src/prisma/generated/prisma/commonInputTypes.ts +++ b/apps/api/src/prisma/generated/prisma/commonInputTypes.ts @@ -162,50 +162,6 @@ export type StringNullableWithAggregatesFilter<$PrismaModel = never> = { _max?: Prisma.NestedStringNullableFilter<$PrismaModel> } -export type BigIntNullableFilter<$PrismaModel = never> = { - equals?: bigint | number | Prisma.BigIntFieldRefInput<$PrismaModel> | null - in?: bigint[] | number[] | Prisma.ListBigIntFieldRefInput<$PrismaModel> | null - notIn?: bigint[] | number[] | Prisma.ListBigIntFieldRefInput<$PrismaModel> | null - lt?: bigint | number | Prisma.BigIntFieldRefInput<$PrismaModel> - lte?: bigint | number | Prisma.BigIntFieldRefInput<$PrismaModel> - gt?: bigint | number | Prisma.BigIntFieldRefInput<$PrismaModel> - gte?: bigint | number | Prisma.BigIntFieldRefInput<$PrismaModel> - not?: Prisma.NestedBigIntNullableFilter<$PrismaModel> | bigint | number | null -} - -export type EnumUploadStatusFilter<$PrismaModel = never> = { - equals?: $Enums.UploadStatus | Prisma.EnumUploadStatusFieldRefInput<$PrismaModel> - in?: $Enums.UploadStatus[] | Prisma.ListEnumUploadStatusFieldRefInput<$PrismaModel> - notIn?: $Enums.UploadStatus[] | Prisma.ListEnumUploadStatusFieldRefInput<$PrismaModel> - not?: Prisma.NestedEnumUploadStatusFilter<$PrismaModel> | $Enums.UploadStatus -} - -export type BigIntNullableWithAggregatesFilter<$PrismaModel = never> = { - equals?: bigint | number | Prisma.BigIntFieldRefInput<$PrismaModel> | null - in?: bigint[] | number[] | Prisma.ListBigIntFieldRefInput<$PrismaModel> | null - notIn?: bigint[] | number[] | Prisma.ListBigIntFieldRefInput<$PrismaModel> | null - lt?: bigint | number | Prisma.BigIntFieldRefInput<$PrismaModel> - lte?: bigint | number | Prisma.BigIntFieldRefInput<$PrismaModel> - gt?: bigint | number | Prisma.BigIntFieldRefInput<$PrismaModel> - gte?: bigint | number | Prisma.BigIntFieldRefInput<$PrismaModel> - not?: Prisma.NestedBigIntNullableWithAggregatesFilter<$PrismaModel> | bigint | number | null - _count?: Prisma.NestedIntNullableFilter<$PrismaModel> - _avg?: Prisma.NestedFloatNullableFilter<$PrismaModel> - _sum?: Prisma.NestedBigIntNullableFilter<$PrismaModel> - _min?: Prisma.NestedBigIntNullableFilter<$PrismaModel> - _max?: Prisma.NestedBigIntNullableFilter<$PrismaModel> -} - -export type EnumUploadStatusWithAggregatesFilter<$PrismaModel = never> = { - equals?: $Enums.UploadStatus | Prisma.EnumUploadStatusFieldRefInput<$PrismaModel> - in?: $Enums.UploadStatus[] | Prisma.ListEnumUploadStatusFieldRefInput<$PrismaModel> - notIn?: $Enums.UploadStatus[] | Prisma.ListEnumUploadStatusFieldRefInput<$PrismaModel> - not?: Prisma.NestedEnumUploadStatusWithAggregatesFilter<$PrismaModel> | $Enums.UploadStatus - _count?: Prisma.NestedIntFilter<$PrismaModel> - _min?: Prisma.NestedEnumUploadStatusFilter<$PrismaModel> - _max?: Prisma.NestedEnumUploadStatusFilter<$PrismaModel> -} - export type NestedStringFilter<$PrismaModel = never> = { equals?: string | Prisma.StringFieldRefInput<$PrismaModel> in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> @@ -367,59 +323,4 @@ export type NestedStringNullableWithAggregatesFilter<$PrismaModel = never> = { _max?: Prisma.NestedStringNullableFilter<$PrismaModel> } -export type NestedBigIntNullableFilter<$PrismaModel = never> = { - equals?: bigint | number | Prisma.BigIntFieldRefInput<$PrismaModel> | null - in?: bigint[] | number[] | Prisma.ListBigIntFieldRefInput<$PrismaModel> | null - notIn?: bigint[] | number[] | Prisma.ListBigIntFieldRefInput<$PrismaModel> | null - lt?: bigint | number | Prisma.BigIntFieldRefInput<$PrismaModel> - lte?: bigint | number | Prisma.BigIntFieldRefInput<$PrismaModel> - gt?: bigint | number | Prisma.BigIntFieldRefInput<$PrismaModel> - gte?: bigint | number | Prisma.BigIntFieldRefInput<$PrismaModel> - not?: Prisma.NestedBigIntNullableFilter<$PrismaModel> | bigint | number | null -} - -export type NestedEnumUploadStatusFilter<$PrismaModel = never> = { - equals?: $Enums.UploadStatus | Prisma.EnumUploadStatusFieldRefInput<$PrismaModel> - in?: $Enums.UploadStatus[] | Prisma.ListEnumUploadStatusFieldRefInput<$PrismaModel> - notIn?: $Enums.UploadStatus[] | Prisma.ListEnumUploadStatusFieldRefInput<$PrismaModel> - not?: Prisma.NestedEnumUploadStatusFilter<$PrismaModel> | $Enums.UploadStatus -} - -export type NestedBigIntNullableWithAggregatesFilter<$PrismaModel = never> = { - equals?: bigint | number | Prisma.BigIntFieldRefInput<$PrismaModel> | null - in?: bigint[] | number[] | Prisma.ListBigIntFieldRefInput<$PrismaModel> | null - notIn?: bigint[] | number[] | Prisma.ListBigIntFieldRefInput<$PrismaModel> | null - lt?: bigint | number | Prisma.BigIntFieldRefInput<$PrismaModel> - lte?: bigint | number | Prisma.BigIntFieldRefInput<$PrismaModel> - gt?: bigint | number | Prisma.BigIntFieldRefInput<$PrismaModel> - gte?: bigint | number | Prisma.BigIntFieldRefInput<$PrismaModel> - not?: Prisma.NestedBigIntNullableWithAggregatesFilter<$PrismaModel> | bigint | number | null - _count?: Prisma.NestedIntNullableFilter<$PrismaModel> - _avg?: Prisma.NestedFloatNullableFilter<$PrismaModel> - _sum?: Prisma.NestedBigIntNullableFilter<$PrismaModel> - _min?: Prisma.NestedBigIntNullableFilter<$PrismaModel> - _max?: Prisma.NestedBigIntNullableFilter<$PrismaModel> -} - -export type NestedFloatNullableFilter<$PrismaModel = never> = { - equals?: number | Prisma.FloatFieldRefInput<$PrismaModel> | null - in?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel> | null - notIn?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel> | null - lt?: number | Prisma.FloatFieldRefInput<$PrismaModel> - lte?: number | Prisma.FloatFieldRefInput<$PrismaModel> - gt?: number | Prisma.FloatFieldRefInput<$PrismaModel> - gte?: number | Prisma.FloatFieldRefInput<$PrismaModel> - not?: Prisma.NestedFloatNullableFilter<$PrismaModel> | number | null -} - -export type NestedEnumUploadStatusWithAggregatesFilter<$PrismaModel = never> = { - equals?: $Enums.UploadStatus | Prisma.EnumUploadStatusFieldRefInput<$PrismaModel> - in?: $Enums.UploadStatus[] | Prisma.ListEnumUploadStatusFieldRefInput<$PrismaModel> - notIn?: $Enums.UploadStatus[] | Prisma.ListEnumUploadStatusFieldRefInput<$PrismaModel> - not?: Prisma.NestedEnumUploadStatusWithAggregatesFilter<$PrismaModel> | $Enums.UploadStatus - _count?: Prisma.NestedIntFilter<$PrismaModel> - _min?: Prisma.NestedEnumUploadStatusFilter<$PrismaModel> - _max?: Prisma.NestedEnumUploadStatusFilter<$PrismaModel> -} - diff --git a/apps/api/src/prisma/generated/prisma/enums.ts b/apps/api/src/prisma/generated/prisma/enums.ts index 49600eb..043572d 100644 --- a/apps/api/src/prisma/generated/prisma/enums.ts +++ b/apps/api/src/prisma/generated/prisma/enums.ts @@ -9,9 +9,7 @@ * 🟢 You can import this file directly. */ -export const UploadStatus = { - SUCCESS: 'SUCCESS', - FAILED: 'FAILED' -} as const -export type UploadStatus = (typeof UploadStatus)[keyof typeof UploadStatus] + +// This file is empty because there are no enums in the schema. +export {} diff --git a/apps/api/src/prisma/generated/prisma/internal/class.ts b/apps/api/src/prisma/generated/prisma/internal/class.ts index e71fc4d..e0dc7df 100644 --- a/apps/api/src/prisma/generated/prisma/internal/class.ts +++ b/apps/api/src/prisma/generated/prisma/internal/class.ts @@ -17,10 +17,10 @@ import type * as Prisma from "./prismaNamespace.js" const config: runtime.GetPrismaClientConfig = { "previewFeatures": [], - "clientVersion": "7.9.1", - "engineVersion": "e922089b7d7502aff4249d5da3420f6fa55fc6ad", + "clientVersion": "7.10.0", + "engineVersion": "0edf323efd1d98336f3f0a68684b56f689b900d3", "activeProvider": "postgresql", - "inlineSchema": "// Prisma schema for dropto.\n// Docs: https://pris.ly/d/prisma-schema\n\ngenerator client {\n provider = \"prisma-client\"\n output = \"./generated/prisma\"\n moduleFormat = \"cjs\"\n}\n\ngenerator dbml {\n provider = \"prisma-dbml-generator\"\n}\n\ndatasource db {\n provider = \"postgresql\"\n}\n\n//\n// Enums\n//\nenum UploadStatus {\n SUCCESS\n FAILED\n}\n\n//\n// Models\n//\n\n/// Single-row store for the operator's token version. Stateless JWTs are otherwise\n/// irrevocable; bumping this version on logout invalidates every access/refresh token\n/// issued before it (the AuthGuard rejects a token whose `ver` no longer matches).\nmodel AuthState {\n id String @id @default(\"singleton\")\n tokenVersion Int @default(0) @map(\"token_version\")\n updatedAt DateTime @updatedAt @map(\"updated_at\")\n\n @@map(\"auth_state\")\n}\n\n/// The single authorized Google account whose Drive quota is used for uploads.\n/// The refresh token is stored encrypted (AES-256-GCM), never in plaintext.\nmodel DriveAccount {\n id String @id @default(uuid())\n email String @unique\n refreshTokenEnc String @map(\"refresh_token_enc\")\n createdAt DateTime @default(now()) @map(\"created_at\")\n updatedAt DateTime @updatedAt @map(\"updated_at\")\n\n allowedFolders AllowedFolder[]\n\n @@map(\"drive_accounts\")\n}\n\n/// A folder the account owner explicitly authorized via the Google Picker.\n/// All Drive operations are validated to stay within this set (or descendants).\nmodel AllowedFolder {\n id String @id @default(uuid())\n driveAccountId String @map(\"drive_account_id\")\n folderId String @map(\"folder_id\")\n name String\n createdAt DateTime @default(now()) @map(\"created_at\")\n\n driveAccount DriveAccount @relation(fields: [driveAccountId], references: [id], onDelete: Cascade)\n\n @@unique([driveAccountId, folderId])\n @@index([driveAccountId], map: \"idx_allowed_folder_drive_account_id\")\n @@map(\"allowed_folders\")\n}\n\n/// One row per issued refresh token, rotated on every refresh. The token itself is an opaque random\n/// secret; only its SHA-256 hash (`tokenHash`) is stored, so a DB leak never exposes usable tokens.\n/// Presenting a token whose row is already revoked signals reuse (theft) → full session revocation.\n/// `expiresAt` is the sliding idle window; `sessionExpiresAt` is the absolute cap inherited across\n/// rotations so a session can't outlive `login + absolute TTL`. `subject` keys rows per operator so\n/// the table is ready for multiple users. Expired rows are pruned on refresh to keep it small.\nmodel RefreshToken {\n id String @id @default(uuid())\n tokenHash String @unique @map(\"token_hash\")\n subject String\n revokedAt DateTime? @map(\"revoked_at\")\n replacedByTokenId String? @map(\"replaced_by_token_id\")\n expiresAt DateTime @map(\"expires_at\")\n sessionExpiresAt DateTime @map(\"session_expires_at\")\n createdAt DateTime @default(now()) @map(\"created_at\")\n\n @@index([subject], map: \"idx_refresh_token_subject\")\n @@index([expiresAt], map: \"idx_refresh_token_expires_at\")\n @@map(\"refresh_tokens\")\n}\n\n/// Audit trail of upload attempts for the history view.\nmodel UploadLog {\n id String @id @default(uuid())\n fileName String @map(\"file_name\")\n folderId String @map(\"folder_id\")\n fileId String? @map(\"file_id\")\n size BigInt?\n status UploadStatus\n error String?\n createdAt DateTime @default(now()) @map(\"created_at\")\n\n @@index([createdAt], map: \"idx_upload_log_created_at\")\n @@map(\"upload_logs\")\n}\n", + "inlineSchema": "// Prisma schema for dropto.\n// Docs: https://pris.ly/d/prisma-schema\n\ngenerator client {\n provider = \"prisma-client\"\n output = \"./generated/prisma\"\n moduleFormat = \"cjs\"\n}\n\ngenerator dbml {\n provider = \"prisma-dbml-generator\"\n}\n\ndatasource db {\n provider = \"postgresql\"\n}\n\n//\n// Models\n//\n\n/// Single-row store for the operator's token version. Stateless JWTs are otherwise\n/// irrevocable; bumping this version on logout invalidates every access/refresh token\n/// issued before it (the AuthGuard rejects a token whose `ver` no longer matches).\nmodel AuthState {\n id String @id @default(\"singleton\")\n tokenVersion Int @default(0) @map(\"token_version\")\n updatedAt DateTime @updatedAt @map(\"updated_at\")\n\n @@map(\"auth_state\")\n}\n\n/// The single authorized Google account whose Drive quota is used for uploads.\n/// The refresh token is stored encrypted (AES-256-GCM), never in plaintext.\nmodel DriveAccount {\n id String @id @default(uuid())\n email String @unique\n refreshTokenEnc String @map(\"refresh_token_enc\")\n createdAt DateTime @default(now()) @map(\"created_at\")\n updatedAt DateTime @updatedAt @map(\"updated_at\")\n\n allowedFolders AllowedFolder[]\n\n @@map(\"drive_accounts\")\n}\n\n/// A folder the account owner explicitly authorized via the Google Picker.\n/// All Drive operations are validated to stay within this set (or descendants).\nmodel AllowedFolder {\n id String @id @default(uuid())\n driveAccountId String @map(\"drive_account_id\")\n folderId String @map(\"folder_id\")\n name String\n createdAt DateTime @default(now()) @map(\"created_at\")\n\n driveAccount DriveAccount @relation(fields: [driveAccountId], references: [id], onDelete: Cascade)\n\n @@unique([driveAccountId, folderId])\n @@index([driveAccountId], map: \"idx_allowed_folder_drive_account_id\")\n @@map(\"allowed_folders\")\n}\n\n/// One row per issued refresh token, rotated on every refresh. The token itself is an opaque random\n/// secret; only its SHA-256 hash (`tokenHash`) is stored, so a DB leak never exposes usable tokens.\n/// Presenting a token whose row is already revoked signals reuse (theft) → full session revocation.\n/// `expiresAt` is the sliding idle window; `sessionExpiresAt` is the absolute cap inherited across\n/// rotations so a session can't outlive `login + absolute TTL`. `subject` keys rows per operator so\n/// the table is ready for multiple users. Expired rows are pruned on refresh to keep it small.\nmodel RefreshToken {\n id String @id @default(uuid())\n tokenHash String @unique @map(\"token_hash\")\n subject String\n revokedAt DateTime? @map(\"revoked_at\")\n replacedByTokenId String? @map(\"replaced_by_token_id\")\n expiresAt DateTime @map(\"expires_at\")\n sessionExpiresAt DateTime @map(\"session_expires_at\")\n createdAt DateTime @default(now()) @map(\"created_at\")\n\n @@index([subject], map: \"idx_refresh_token_subject\")\n @@index([expiresAt], map: \"idx_refresh_token_expires_at\")\n @@map(\"refresh_tokens\")\n}\n", "runtimeDataModel": { "models": {}, "enums": {}, @@ -32,10 +32,10 @@ const config: runtime.GetPrismaClientConfig = { } } -config.runtimeDataModel = JSON.parse("{\"models\":{\"AuthState\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"tokenVersion\",\"kind\":\"scalar\",\"type\":\"Int\",\"dbName\":\"token_version\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\",\"dbName\":\"updated_at\"}],\"dbName\":\"auth_state\"},\"DriveAccount\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"email\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"refreshTokenEnc\",\"kind\":\"scalar\",\"type\":\"String\",\"dbName\":\"refresh_token_enc\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\",\"dbName\":\"created_at\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\",\"dbName\":\"updated_at\"},{\"name\":\"allowedFolders\",\"kind\":\"object\",\"type\":\"AllowedFolder\",\"relationName\":\"AllowedFolderToDriveAccount\"}],\"dbName\":\"drive_accounts\"},\"AllowedFolder\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"driveAccountId\",\"kind\":\"scalar\",\"type\":\"String\",\"dbName\":\"drive_account_id\"},{\"name\":\"folderId\",\"kind\":\"scalar\",\"type\":\"String\",\"dbName\":\"folder_id\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\",\"dbName\":\"created_at\"},{\"name\":\"driveAccount\",\"kind\":\"object\",\"type\":\"DriveAccount\",\"relationName\":\"AllowedFolderToDriveAccount\"}],\"dbName\":\"allowed_folders\"},\"RefreshToken\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"tokenHash\",\"kind\":\"scalar\",\"type\":\"String\",\"dbName\":\"token_hash\"},{\"name\":\"subject\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"revokedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\",\"dbName\":\"revoked_at\"},{\"name\":\"replacedByTokenId\",\"kind\":\"scalar\",\"type\":\"String\",\"dbName\":\"replaced_by_token_id\"},{\"name\":\"expiresAt\",\"kind\":\"scalar\",\"type\":\"DateTime\",\"dbName\":\"expires_at\"},{\"name\":\"sessionExpiresAt\",\"kind\":\"scalar\",\"type\":\"DateTime\",\"dbName\":\"session_expires_at\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\",\"dbName\":\"created_at\"}],\"dbName\":\"refresh_tokens\"},\"UploadLog\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"fileName\",\"kind\":\"scalar\",\"type\":\"String\",\"dbName\":\"file_name\"},{\"name\":\"folderId\",\"kind\":\"scalar\",\"type\":\"String\",\"dbName\":\"folder_id\"},{\"name\":\"fileId\",\"kind\":\"scalar\",\"type\":\"String\",\"dbName\":\"file_id\"},{\"name\":\"size\",\"kind\":\"scalar\",\"type\":\"BigInt\"},{\"name\":\"status\",\"kind\":\"enum\",\"type\":\"UploadStatus\"},{\"name\":\"error\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\",\"dbName\":\"created_at\"}],\"dbName\":\"upload_logs\"}},\"enums\":{},\"types\":{}}") +config.runtimeDataModel = JSON.parse("{\"models\":{\"AuthState\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"tokenVersion\",\"kind\":\"scalar\",\"type\":\"Int\",\"dbName\":\"token_version\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\",\"dbName\":\"updated_at\"}],\"dbName\":\"auth_state\",\"schema\":null},\"DriveAccount\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"email\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"refreshTokenEnc\",\"kind\":\"scalar\",\"type\":\"String\",\"dbName\":\"refresh_token_enc\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\",\"dbName\":\"created_at\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\",\"dbName\":\"updated_at\"},{\"name\":\"allowedFolders\",\"kind\":\"object\",\"type\":\"AllowedFolder\",\"relationName\":\"AllowedFolderToDriveAccount\"}],\"dbName\":\"drive_accounts\",\"schema\":null},\"AllowedFolder\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"driveAccountId\",\"kind\":\"scalar\",\"type\":\"String\",\"dbName\":\"drive_account_id\"},{\"name\":\"folderId\",\"kind\":\"scalar\",\"type\":\"String\",\"dbName\":\"folder_id\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\",\"dbName\":\"created_at\"},{\"name\":\"driveAccount\",\"kind\":\"object\",\"type\":\"DriveAccount\",\"relationName\":\"AllowedFolderToDriveAccount\"}],\"dbName\":\"allowed_folders\",\"schema\":null},\"RefreshToken\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"tokenHash\",\"kind\":\"scalar\",\"type\":\"String\",\"dbName\":\"token_hash\"},{\"name\":\"subject\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"revokedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\",\"dbName\":\"revoked_at\"},{\"name\":\"replacedByTokenId\",\"kind\":\"scalar\",\"type\":\"String\",\"dbName\":\"replaced_by_token_id\"},{\"name\":\"expiresAt\",\"kind\":\"scalar\",\"type\":\"DateTime\",\"dbName\":\"expires_at\"},{\"name\":\"sessionExpiresAt\",\"kind\":\"scalar\",\"type\":\"DateTime\",\"dbName\":\"session_expires_at\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\",\"dbName\":\"created_at\"}],\"dbName\":\"refresh_tokens\",\"schema\":null}},\"enums\":{},\"types\":{}}") config.parameterizationSchema = { - strings: JSON.parse("[\"where\",\"AuthState.findUnique\",\"AuthState.findUniqueOrThrow\",\"orderBy\",\"cursor\",\"AuthState.findFirst\",\"AuthState.findFirstOrThrow\",\"AuthState.findMany\",\"data\",\"AuthState.createOne\",\"AuthState.createMany\",\"AuthState.createManyAndReturn\",\"AuthState.updateOne\",\"AuthState.updateMany\",\"AuthState.updateManyAndReturn\",\"create\",\"update\",\"AuthState.upsertOne\",\"AuthState.deleteOne\",\"AuthState.deleteMany\",\"having\",\"_count\",\"_avg\",\"_sum\",\"_min\",\"_max\",\"AuthState.groupBy\",\"AuthState.aggregate\",\"driveAccount\",\"allowedFolders\",\"DriveAccount.findUnique\",\"DriveAccount.findUniqueOrThrow\",\"DriveAccount.findFirst\",\"DriveAccount.findFirstOrThrow\",\"DriveAccount.findMany\",\"DriveAccount.createOne\",\"DriveAccount.createMany\",\"DriveAccount.createManyAndReturn\",\"DriveAccount.updateOne\",\"DriveAccount.updateMany\",\"DriveAccount.updateManyAndReturn\",\"DriveAccount.upsertOne\",\"DriveAccount.deleteOne\",\"DriveAccount.deleteMany\",\"DriveAccount.groupBy\",\"DriveAccount.aggregate\",\"AllowedFolder.findUnique\",\"AllowedFolder.findUniqueOrThrow\",\"AllowedFolder.findFirst\",\"AllowedFolder.findFirstOrThrow\",\"AllowedFolder.findMany\",\"AllowedFolder.createOne\",\"AllowedFolder.createMany\",\"AllowedFolder.createManyAndReturn\",\"AllowedFolder.updateOne\",\"AllowedFolder.updateMany\",\"AllowedFolder.updateManyAndReturn\",\"AllowedFolder.upsertOne\",\"AllowedFolder.deleteOne\",\"AllowedFolder.deleteMany\",\"AllowedFolder.groupBy\",\"AllowedFolder.aggregate\",\"RefreshToken.findUnique\",\"RefreshToken.findUniqueOrThrow\",\"RefreshToken.findFirst\",\"RefreshToken.findFirstOrThrow\",\"RefreshToken.findMany\",\"RefreshToken.createOne\",\"RefreshToken.createMany\",\"RefreshToken.createManyAndReturn\",\"RefreshToken.updateOne\",\"RefreshToken.updateMany\",\"RefreshToken.updateManyAndReturn\",\"RefreshToken.upsertOne\",\"RefreshToken.deleteOne\",\"RefreshToken.deleteMany\",\"RefreshToken.groupBy\",\"RefreshToken.aggregate\",\"UploadLog.findUnique\",\"UploadLog.findUniqueOrThrow\",\"UploadLog.findFirst\",\"UploadLog.findFirstOrThrow\",\"UploadLog.findMany\",\"UploadLog.createOne\",\"UploadLog.createMany\",\"UploadLog.createManyAndReturn\",\"UploadLog.updateOne\",\"UploadLog.updateMany\",\"UploadLog.updateManyAndReturn\",\"UploadLog.upsertOne\",\"UploadLog.deleteOne\",\"UploadLog.deleteMany\",\"UploadLog.groupBy\",\"UploadLog.aggregate\",\"AND\",\"OR\",\"NOT\",\"id\",\"fileName\",\"folderId\",\"fileId\",\"size\",\"UploadStatus\",\"status\",\"error\",\"createdAt\",\"equals\",\"in\",\"notIn\",\"lt\",\"lte\",\"gt\",\"gte\",\"not\",\"contains\",\"startsWith\",\"endsWith\",\"tokenHash\",\"subject\",\"revokedAt\",\"replacedByTokenId\",\"expiresAt\",\"sessionExpiresAt\",\"driveAccountId\",\"name\",\"email\",\"refreshTokenEnc\",\"updatedAt\",\"every\",\"some\",\"none\",\"driveAccountId_folderId\",\"tokenVersion\",\"is\",\"isNot\",\"connectOrCreate\",\"upsert\",\"createMany\",\"set\",\"disconnect\",\"delete\",\"connect\",\"updateMany\",\"deleteMany\",\"increment\",\"decrement\",\"multiply\",\"divide\"]"), - graph: "3gEuUAZeAACqAQAwXwAABAAQYAAAqgEAMGEBAAAAAX9AAJgBACGEAQIAqwEAIQEAAAABACABAAAAAQAgBl4AAKoBADBfAAAEABBgAACqAQAwYQEAlAEAIX9AAJgBACGEAQIAqwEAIQADAAAABAAgAwAABQAwBAAAAQAgAwAAAAQAIAMAAAUAMAQAAAEAIAMAAAAEACADAAAFADAEAAABACADYQEAAAABf0AAAAABhAECAAAAAQEIAAAJACADYQEAAAABf0AAAAABhAECAAAAAQEIAAALADABCAAACwAwA2EBALIBACF_QAC2AQAhhAECANgBACECAAAAAQAgCAAADgAgA2EBALIBACF_QAC2AQAhhAECANgBACECAAAABAAgCAAAEAAgAgAAAAQAIAgAABAAIAMAAAABACAPAAAJACAQAAAOACABAAAAAQAgAQAAAAQAIAUVAADTAQAgFgAA1AEAIBcAANcBACAYAADWAQAgGQAA1QEAIAZeAACmAQAwXwAAFwAQYAAApgEAMGEBAIEBACF_QACFAQAhhAECAKcBACEDAAAABAAgAwAAFgAwFAAAFwAgAwAAAAQAIAMAAAUAMAQAAAEAIAkdAACiAQAgXgAAoQEAMF8AACIAEGAAAKEBADBhAQAAAAFpQACYAQAhfQEAAAABfgEAlAEAIX9AAJgBACEBAAAAGgAgCRwAAKUBACBeAACkAQAwXwAAHAAQYAAApAEAMGEBAJQBACFjAQCUAQAhaUAAmAEAIXsBAJQBACF8AQCUAQAhARwAANIBACAKHAAApQEAIF4AAKQBADBfAAAcABBgAACkAQAwYQEAAAABYwEAlAEAIWlAAJgBACF7AQCUAQAhfAEAlAEAIYMBAACjAQAgAwAAABwAIAMAAB0AMAQAAB4AIAEAAAAcACABAAAAGgAgCR0AAKIBACBeAAChAQAwXwAAIgAQYAAAoQEAMGEBAJQBACFpQACYAQAhfQEAlAEAIX4BAJQBACF_QACYAQAhAR0AANEBACADAAAAIgAgAwAAIwAwBAAAGgAgAwAAACIAIAMAACMAMAQAABoAIAMAAAAiACADAAAjADAEAAAaACAGHQAA0AEAIGEBAAAAAWlAAAAAAX0BAAAAAX4BAAAAAX9AAAAAAQEIAAAnACAFYQEAAAABaUAAAAABfQEAAAABfgEAAAABf0AAAAABAQgAACkAMAEIAAApADAGHQAAwwEAIGEBALIBACFpQAC2AQAhfQEAsgEAIX4BALIBACF_QAC2AQAhAgAAABoAIAgAACwAIAVhAQCyAQAhaUAAtgEAIX0BALIBACF-AQCyAQAhf0AAtgEAIQIAAAAiACAIAAAuACACAAAAIgAgCAAALgAgAwAAABoAIA8AACcAIBAAACwAIAEAAAAaACABAAAAIgAgAxUAAMABACAYAADCAQAgGQAAwQEAIAheAACgAQAwXwAANQAQYAAAoAEAMGEBAIEBACFpQACFAQAhfQEAgQEAIX4BAIEBACF_QACFAQAhAwAAACIAIAMAADQAMBQAADUAIAMAAAAiACADAAAjADAEAAAaACABAAAAHgAgAQAAAB4AIAMAAAAcACADAAAdADAEAAAeACADAAAAHAAgAwAAHQAwBAAAHgAgAwAAABwAIAMAAB0AMAQAAB4AIAYcAAC_AQAgYQEAAAABYwEAAAABaUAAAAABewEAAAABfAEAAAABAQgAAD0AIAVhAQAAAAFjAQAAAAFpQAAAAAF7AQAAAAF8AQAAAAEBCAAAPwAwAQgAAD8AMAYcAAC-AQAgYQEAsgEAIWMBALIBACFpQAC2AQAhewEAsgEAIXwBALIBACECAAAAHgAgCAAAQgAgBWEBALIBACFjAQCyAQAhaUAAtgEAIXsBALIBACF8AQCyAQAhAgAAABwAIAgAAEQAIAIAAAAcACAIAABEACADAAAAHgAgDwAAPQAgEAAAQgAgAQAAAB4AIAEAAAAcACADFQAAuwEAIBgAAL0BACAZAAC8AQAgCF4AAJ8BADBfAABLABBgAACfAQAwYQEAgQEAIWMBAIEBACFpQACFAQAhewEAgQEAIXwBAIEBACEDAAAAHAAgAwAASgAwFAAASwAgAwAAABwAIAMAAB0AMAQAAB4AIAteAACdAQAwXwAAUQAQYAAAnQEAMGEBAAAAAWlAAJgBACF1AQAAAAF2AQCUAQAhd0AAngEAIXgBAJUBACF5QACYAQAhekAAmAEAIQEAAABOACABAAAATgAgC14AAJ0BADBfAABRABBgAACdAQAwYQEAlAEAIWlAAJgBACF1AQCUAQAhdgEAlAEAIXdAAJ4BACF4AQCVAQAheUAAmAEAIXpAAJgBACECdwAArAEAIHgAAKwBACADAAAAUQAgAwAAUgAwBAAATgAgAwAAAFEAIAMAAFIAMAQAAE4AIAMAAABRACADAABSADAEAABOACAIYQEAAAABaUAAAAABdQEAAAABdgEAAAABd0AAAAABeAEAAAABeUAAAAABekAAAAABAQgAAFYAIAhhAQAAAAFpQAAAAAF1AQAAAAF2AQAAAAF3QAAAAAF4AQAAAAF5QAAAAAF6QAAAAAEBCAAAWAAwAQgAAFgAMAhhAQCyAQAhaUAAtgEAIXUBALIBACF2AQCyAQAhd0AAugEAIXgBALMBACF5QAC2AQAhekAAtgEAIQIAAABOACAIAABbACAIYQEAsgEAIWlAALYBACF1AQCyAQAhdgEAsgEAIXdAALoBACF4AQCzAQAheUAAtgEAIXpAALYBACECAAAAUQAgCAAAXQAgAgAAAFEAIAgAAF0AIAMAAABOACAPAABWACAQAABbACABAAAATgAgAQAAAFEAIAUVAAC3AQAgGAAAuQEAIBkAALgBACB3AACsAQAgeAAArAEAIAteAACZAQAwXwAAZAAQYAAAmQEAMGEBAIEBACFpQACFAQAhdQEAgQEAIXYBAIEBACF3QACaAQAheAEAggEAIXlAAIUBACF6QACFAQAhAwAAAFEAIAMAAGMAMBQAAGQAIAMAAABRACADAABSADAEAABOACALXgAAkwEAMF8AAGoAEGAAAJMBADBhAQAAAAFiAQCUAQAhYwEAlAEAIWQBAJUBACFlBACWAQAhZwAAlwFnImgBAJUBACFpQACYAQAhAQAAAGcAIAEAAABnACALXgAAkwEAMF8AAGoAEGAAAJMBADBhAQCUAQAhYgEAlAEAIWMBAJQBACFkAQCVAQAhZQQAlgEAIWcAAJcBZyJoAQCVAQAhaUAAmAEAIQNkAACsAQAgZQAArAEAIGgAAKwBACADAAAAagAgAwAAawAwBAAAZwAgAwAAAGoAIAMAAGsAMAQAAGcAIAMAAABqACADAABrADAEAABnACAIYQEAAAABYgEAAAABYwEAAAABZAEAAAABZQQAAAABZwAAAGcCaAEAAAABaUAAAAABAQgAAG8AIAhhAQAAAAFiAQAAAAFjAQAAAAFkAQAAAAFlBAAAAAFnAAAAZwJoAQAAAAFpQAAAAAEBCAAAcQAwAQgAAHEAMAhhAQCyAQAhYgEAsgEAIWMBALIBACFkAQCzAQAhZQQAtAEAIWcAALUBZyJoAQCzAQAhaUAAtgEAIQIAAABnACAIAAB0ACAIYQEAsgEAIWIBALIBACFjAQCyAQAhZAEAswEAIWUEALQBACFnAAC1AWciaAEAswEAIWlAALYBACECAAAAagAgCAAAdgAgAgAAAGoAIAgAAHYAIAMAAABnACAPAABvACAQAAB0ACABAAAAZwAgAQAAAGoAIAgVAACtAQAgFgAArgEAIBcAALEBACAYAACwAQAgGQAArwEAIGQAAKwBACBlAACsAQAgaAAArAEAIAteAACAAQAwXwAAfQAQYAAAgAEAMGEBAIEBACFiAQCBAQAhYwEAgQEAIWQBAIIBACFlBACDAQAhZwAAhAFnImgBAIIBACFpQACFAQAhAwAAAGoAIAMAAHwAMBQAAH0AIAMAAABqACADAABrADAEAABnACALXgAAgAEAMF8AAH0AEGAAAIABADBhAQCBAQAhYgEAgQEAIWMBAIEBACFkAQCCAQAhZQQAgwEAIWcAAIQBZyJoAQCCAQAhaUAAhQEAIQ4VAACHAQAgGAAAkgEAIBkAAJIBACBqAQAAAAFrAQAAAARsAQAAAARtAQAAAAFuAQAAAAFvAQAAAAFwAQAAAAFxAQCRAQAhcgEAAAABcwEAAAABdAEAAAABDhUAAIwBACAYAACQAQAgGQAAkAEAIGoBAAAAAWsBAAAABWwBAAAABW0BAAAAAW4BAAAAAW8BAAAAAXABAAAAAXEBAI8BACFyAQAAAAFzAQAAAAF0AQAAAAENFQAAjAEAIBYAAI0BACAXAACOAQAgGAAAjgEAIBkAAI4BACBqBAAAAAFrBAAAAAVsBAAAAAVtBAAAAAFuBAAAAAFvBAAAAAFwBAAAAAFxBACLAQAhBxUAAIcBACAYAACKAQAgGQAAigEAIGoAAABnAmsAAABnCGwAAABnCHEAAIkBZyILFQAAhwEAIBgAAIgBACAZAACIAQAgakAAAAABa0AAAAAEbEAAAAAEbUAAAAABbkAAAAABb0AAAAABcEAAAAABcUAAhgEAIQsVAACHAQAgGAAAiAEAIBkAAIgBACBqQAAAAAFrQAAAAARsQAAAAARtQAAAAAFuQAAAAAFvQAAAAAFwQAAAAAFxQACGAQAhCGoCAAAAAWsCAAAABGwCAAAABG0CAAAAAW4CAAAAAW8CAAAAAXACAAAAAXECAIcBACEIakAAAAABa0AAAAAEbEAAAAAEbUAAAAABbkAAAAABb0AAAAABcEAAAAABcUAAiAEAIQcVAACHAQAgGAAAigEAIBkAAIoBACBqAAAAZwJrAAAAZwhsAAAAZwhxAACJAWciBGoAAABnAmsAAABnCGwAAABnCHEAAIoBZyINFQAAjAEAIBYAAI0BACAXAACOAQAgGAAAjgEAIBkAAI4BACBqBAAAAAFrBAAAAAVsBAAAAAVtBAAAAAFuBAAAAAFvBAAAAAFwBAAAAAFxBACLAQAhCGoCAAAAAWsCAAAABWwCAAAABW0CAAAAAW4CAAAAAW8CAAAAAXACAAAAAXECAIwBACEIaggAAAABawgAAAAFbAgAAAAFbQgAAAABbggAAAABbwgAAAABcAgAAAABcQgAjQEAIQhqBAAAAAFrBAAAAAVsBAAAAAVtBAAAAAFuBAAAAAFvBAAAAAFwBAAAAAFxBACOAQAhDhUAAIwBACAYAACQAQAgGQAAkAEAIGoBAAAAAWsBAAAABWwBAAAABW0BAAAAAW4BAAAAAW8BAAAAAXABAAAAAXEBAI8BACFyAQAAAAFzAQAAAAF0AQAAAAELagEAAAABawEAAAAFbAEAAAAFbQEAAAABbgEAAAABbwEAAAABcAEAAAABcQEAkAEAIXIBAAAAAXMBAAAAAXQBAAAAAQ4VAACHAQAgGAAAkgEAIBkAAJIBACBqAQAAAAFrAQAAAARsAQAAAARtAQAAAAFuAQAAAAFvAQAAAAFwAQAAAAFxAQCRAQAhcgEAAAABcwEAAAABdAEAAAABC2oBAAAAAWsBAAAABGwBAAAABG0BAAAAAW4BAAAAAW8BAAAAAXABAAAAAXEBAJIBACFyAQAAAAFzAQAAAAF0AQAAAAELXgAAkwEAMF8AAGoAEGAAAJMBADBhAQCUAQAhYgEAlAEAIWMBAJQBACFkAQCVAQAhZQQAlgEAIWcAAJcBZyJoAQCVAQAhaUAAmAEAIQtqAQAAAAFrAQAAAARsAQAAAARtAQAAAAFuAQAAAAFvAQAAAAFwAQAAAAFxAQCSAQAhcgEAAAABcwEAAAABdAEAAAABC2oBAAAAAWsBAAAABWwBAAAABW0BAAAAAW4BAAAAAW8BAAAAAXABAAAAAXEBAJABACFyAQAAAAFzAQAAAAF0AQAAAAEIagQAAAABawQAAAAFbAQAAAAFbQQAAAABbgQAAAABbwQAAAABcAQAAAABcQQAjgEAIQRqAAAAZwJrAAAAZwhsAAAAZwhxAACKAWciCGpAAAAAAWtAAAAABGxAAAAABG1AAAAAAW5AAAAAAW9AAAAAAXBAAAAAAXFAAIgBACELXgAAmQEAMF8AAGQAEGAAAJkBADBhAQCBAQAhaUAAhQEAIXUBAIEBACF2AQCBAQAhd0AAmgEAIXgBAIIBACF5QACFAQAhekAAhQEAIQsVAACMAQAgGAAAnAEAIBkAAJwBACBqQAAAAAFrQAAAAAVsQAAAAAVtQAAAAAFuQAAAAAFvQAAAAAFwQAAAAAFxQACbAQAhCxUAAIwBACAYAACcAQAgGQAAnAEAIGpAAAAAAWtAAAAABWxAAAAABW1AAAAAAW5AAAAAAW9AAAAAAXBAAAAAAXFAAJsBACEIakAAAAABa0AAAAAFbEAAAAAFbUAAAAABbkAAAAABb0AAAAABcEAAAAABcUAAnAEAIQteAACdAQAwXwAAUQAQYAAAnQEAMGEBAJQBACFpQACYAQAhdQEAlAEAIXYBAJQBACF3QACeAQAheAEAlQEAIXlAAJgBACF6QACYAQAhCGpAAAAAAWtAAAAABWxAAAAABW1AAAAAAW5AAAAAAW9AAAAAAXBAAAAAAXFAAJwBACEIXgAAnwEAMF8AAEsAEGAAAJ8BADBhAQCBAQAhYwEAgQEAIWlAAIUBACF7AQCBAQAhfAEAgQEAIQheAACgAQAwXwAANQAQYAAAoAEAMGEBAIEBACFpQACFAQAhfQEAgQEAIX4BAIEBACF_QACFAQAhCR0AAKIBACBeAAChAQAwXwAAIgAQYAAAoQEAMGEBAJQBACFpQACYAQAhfQEAlAEAIX4BAJQBACF_QACYAQAhA4ABAAAcACCBAQAAHAAgggEAABwAIAJjAQAAAAF7AQAAAAEJHAAApQEAIF4AAKQBADBfAAAcABBgAACkAQAwYQEAlAEAIWMBAJQBACFpQACYAQAhewEAlAEAIXwBAJQBACELHQAAogEAIF4AAKEBADBfAAAiABBgAAChAQAwYQEAlAEAIWlAAJgBACF9AQCUAQAhfgEAlAEAIX9AAJgBACGFAQAAIgAghgEAACIAIAZeAACmAQAwXwAAFwAQYAAApgEAMGEBAIEBACF_QACFAQAhhAECAKcBACENFQAAhwEAIBYAAKkBACAXAACHAQAgGAAAhwEAIBkAAIcBACBqAgAAAAFrAgAAAARsAgAAAARtAgAAAAFuAgAAAAFvAgAAAAFwAgAAAAFxAgCoAQAhDRUAAIcBACAWAACpAQAgFwAAhwEAIBgAAIcBACAZAACHAQAgagIAAAABawIAAAAEbAIAAAAEbQIAAAABbgIAAAABbwIAAAABcAIAAAABcQIAqAEAIQhqCAAAAAFrCAAAAARsCAAAAARtCAAAAAFuCAAAAAFvCAAAAAFwCAAAAAFxCACpAQAhBl4AAKoBADBfAAAEABBgAACqAQAwYQEAlAEAIX9AAJgBACGEAQIAqwEAIQhqAgAAAAFrAgAAAARsAgAAAARtAgAAAAFuAgAAAAFvAgAAAAFwAgAAAAFxAgCHAQAhAAAAAAAAAYoBAQAAAAEBigEBAAAAAQWKAQQAAAABkAEEAAAAAZEBBAAAAAGSAQQAAAABkwEEAAAAAQGKAQAAAGcCAYoBQAAAAAEAAAABigFAAAAAAQAAAAUPAADaAQAgEAAA3QEAIIcBAADbAQAgiAEAANwBACCNAQAAGgAgAw8AANoBACCHAQAA2wEAII0BAAAaACAAAAALDwAAxAEAMBAAAMkBADCHAQAAxQEAMIgBAADGAQAwiQEAAMcBACCKAQAAyAEAMIsBAADIAQAwjAEAAMgBADCNAQAAyAEAMI4BAADKAQAwjwEAAMsBADAEYQEAAAABYwEAAAABaUAAAAABfAEAAAABAgAAAB4AIA8AAM8BACADAAAAHgAgDwAAzwEAIBAAAM4BACABCAAA2QEAMAocAAClAQAgXgAApAEAMF8AABwAEGAAAKQBADBhAQAAAAFjAQCUAQAhaUAAmAEAIXsBAJQBACF8AQCUAQAhgwEAAKMBACACAAAAHgAgCAAAzgEAIAIAAADMAQAgCAAAzQEAIAheAADLAQAwXwAAzAEAEGAAAMsBADBhAQCUAQAhYwEAlAEAIWlAAJgBACF7AQCUAQAhfAEAlAEAIQheAADLAQAwXwAAzAEAEGAAAMsBADBhAQCUAQAhYwEAlAEAIWlAAJgBACF7AQCUAQAhfAEAlAEAIQRhAQCyAQAhYwEAsgEAIWlAALYBACF8AQCyAQAhBGEBALIBACFjAQCyAQAhaUAAtgEAIXwBALIBACEEYQEAAAABYwEAAAABaUAAAAABfAEAAAABBA8AAMQBADCHAQAAxQEAMIkBAADHAQAgjQEAAMgBADAAAR0AANEBACAAAAAAAAWKAQIAAAABkAECAAAAAZEBAgAAAAGSAQIAAAABkwECAAAAAQRhAQAAAAFjAQAAAAFpQAAAAAF8AQAAAAEFYQEAAAABaUAAAAABfQEAAAABfgEAAAABf0AAAAABAgAAABoAIA8AANoBACADAAAAIgAgDwAA2gEAIBAAAN4BACAHAAAAIgAgCAAA3gEAIGEBALIBACFpQAC2AQAhfQEAsgEAIX4BALIBACF_QAC2AQAhBWEBALIBACFpQAC2AQAhfQEAsgEAIX4BALIBACF_QAC2AQAhAAAAAAUVAAYWAAcXAAgYAAkZAAoAAAAAAAUVAAYWAAcXAAgYAAkZAAoCFQAOHR8NARwADAEdIAAAAAMVABIYABMZABQAAAADFQASGAATGQAUARwADAEcAAwDFQAZGAAaGQAbAAAAAxUAGRgAGhkAGwAAAAMVACEYACIZACMAAAADFQAhGAAiGQAjAAAABRUAKRYAKhcAKxgALBkALQAAAAAABRUAKRYAKhcAKxgALBkALQECAQIDAQUGAQYHAQcIAQkKAQoMAgsNAwwPAQ0RAg4SBBETARIUARMVAhoYBRsZCx4bDB8hDCAkDCElDCImDCMoDCQqAiUrDyYtDCcvAigwECkxDCoyDCszAiw2ES03FS44DS85DTA6DTE7DTI8DTM-DTRAAjVBFjZDDTdFAjhGFzlHDTpIDTtJAjxMGD1NHD5PHT9QHUBTHUFUHUJVHUNXHURZAkVaHkZcHUdeAkhfH0lgHUphHUtiAkxlIE1mJE5oJU9pJVBsJVFtJVJuJVNwJVRyAlVzJlZ1JVd3Alh4J1l5JVp6JVt7Alx-KF1_Lg" + strings: JSON.parse("[\"where\",\"AuthState.findUnique\",\"AuthState.findUniqueOrThrow\",\"orderBy\",\"cursor\",\"AuthState.findFirst\",\"AuthState.findFirstOrThrow\",\"AuthState.findMany\",\"data\",\"AuthState.createOne\",\"AuthState.createMany\",\"AuthState.createManyAndReturn\",\"AuthState.updateOne\",\"AuthState.updateMany\",\"AuthState.updateManyAndReturn\",\"create\",\"update\",\"AuthState.upsertOne\",\"AuthState.deleteOne\",\"AuthState.deleteMany\",\"having\",\"_count\",\"_avg\",\"_sum\",\"_min\",\"_max\",\"AuthState.groupBy\",\"AuthState.aggregate\",\"driveAccount\",\"allowedFolders\",\"DriveAccount.findUnique\",\"DriveAccount.findUniqueOrThrow\",\"DriveAccount.findFirst\",\"DriveAccount.findFirstOrThrow\",\"DriveAccount.findMany\",\"DriveAccount.createOne\",\"DriveAccount.createMany\",\"DriveAccount.createManyAndReturn\",\"DriveAccount.updateOne\",\"DriveAccount.updateMany\",\"DriveAccount.updateManyAndReturn\",\"DriveAccount.upsertOne\",\"DriveAccount.deleteOne\",\"DriveAccount.deleteMany\",\"DriveAccount.groupBy\",\"DriveAccount.aggregate\",\"AllowedFolder.findUnique\",\"AllowedFolder.findUniqueOrThrow\",\"AllowedFolder.findFirst\",\"AllowedFolder.findFirstOrThrow\",\"AllowedFolder.findMany\",\"AllowedFolder.createOne\",\"AllowedFolder.createMany\",\"AllowedFolder.createManyAndReturn\",\"AllowedFolder.updateOne\",\"AllowedFolder.updateMany\",\"AllowedFolder.updateManyAndReturn\",\"AllowedFolder.upsertOne\",\"AllowedFolder.deleteOne\",\"AllowedFolder.deleteMany\",\"AllowedFolder.groupBy\",\"AllowedFolder.aggregate\",\"RefreshToken.findUnique\",\"RefreshToken.findUniqueOrThrow\",\"RefreshToken.findFirst\",\"RefreshToken.findFirstOrThrow\",\"RefreshToken.findMany\",\"RefreshToken.createOne\",\"RefreshToken.createMany\",\"RefreshToken.createManyAndReturn\",\"RefreshToken.updateOne\",\"RefreshToken.updateMany\",\"RefreshToken.updateManyAndReturn\",\"RefreshToken.upsertOne\",\"RefreshToken.deleteOne\",\"RefreshToken.deleteMany\",\"RefreshToken.groupBy\",\"RefreshToken.aggregate\",\"AND\",\"OR\",\"NOT\",\"id\",\"tokenHash\",\"subject\",\"revokedAt\",\"replacedByTokenId\",\"expiresAt\",\"sessionExpiresAt\",\"createdAt\",\"equals\",\"in\",\"notIn\",\"lt\",\"lte\",\"gt\",\"gte\",\"not\",\"contains\",\"startsWith\",\"endsWith\",\"driveAccountId\",\"folderId\",\"name\",\"email\",\"refreshTokenEnc\",\"updatedAt\",\"every\",\"some\",\"none\",\"driveAccountId_folderId\",\"tokenVersion\",\"is\",\"isNot\",\"connectOrCreate\",\"upsert\",\"createMany\",\"set\",\"disconnect\",\"delete\",\"connect\",\"updateMany\",\"deleteMany\",\"increment\",\"decrement\",\"multiply\",\"divide\"]"), + graph: "swEkQAZOAACGAQAwTwAABAAQUAAAhgEAMFEBAAAAAWlAAHoAIW4CAIcBACEBAAAAAQAgAQAAAAEAIAZOAACGAQAwTwAABAAQUAAAhgEAMFEBAHcAIWlAAHoAIW4CAIcBACEAAwAAAAQAIAMAAAUAMAQAAAEAIAMAAAAEACADAAAFADAEAAABACADAAAABAAgAwAABQAwBAAAAQAgA1EBAAAAAWlAAAAAAW4CAAAAAQEIAAAJACADUQEAAAABaUAAAAABbgIAAAABAQgAAAsAMAEIAAALADADUQEAjAEAIWlAAI8BACFuAgCtAQAhAgAAAAEAIAgAAA4AIANRAQCMAQAhaUAAjwEAIW4CAK0BACECAAAABAAgCAAAEAAgAgAAAAQAIAgAABAAIAMAAAABACAPAAAJACAQAAAOACABAAAAAQAgAQAAAAQAIAUVAACoAQAgFgAAqQEAIBcAAKwBACAYAACrAQAgGQAAqgEAIAZOAACCAQAwTwAAFwAQUAAAggEAMFEBAGgAIWlAAGsAIW4CAIMBACEDAAAABAAgAwAAFgAwFAAAFwAgAwAAAAQAIAMAAAUAMAQAAAEAIAkdAAB-ACBOAAB9ADBPAAAiABBQAAB9ADBRAQAAAAFYQAB6ACFnAQAAAAFoAQB3ACFpQAB6ACEBAAAAGgAgCRwAAIEBACBOAACAAQAwTwAAHAAQUAAAgAEAMFEBAHcAIVhAAHoAIWQBAHcAIWUBAHcAIWYBAHcAIQEcAACnAQAgChwAAIEBACBOAACAAQAwTwAAHAAQUAAAgAEAMFEBAAAAAVhAAHoAIWQBAHcAIWUBAHcAIWYBAHcAIW0AAH8AIAMAAAAcACADAAAdADAEAAAeACABAAAAHAAgAQAAABoAIAkdAAB-ACBOAAB9ADBPAAAiABBQAAB9ADBRAQB3ACFYQAB6ACFnAQB3ACFoAQB3ACFpQAB6ACEBHQAApgEAIAMAAAAiACADAAAjADAEAAAaACADAAAAIgAgAwAAIwAwBAAAGgAgAwAAACIAIAMAACMAMAQAABoAIAYdAAClAQAgUQEAAAABWEAAAAABZwEAAAABaAEAAAABaUAAAAABAQgAACcAIAVRAQAAAAFYQAAAAAFnAQAAAAFoAQAAAAFpQAAAAAEBCAAAKQAwAQgAACkAMAYdAACYAQAgUQEAjAEAIVhAAI8BACFnAQCMAQAhaAEAjAEAIWlAAI8BACECAAAAGgAgCAAALAAgBVEBAIwBACFYQACPAQAhZwEAjAEAIWgBAIwBACFpQACPAQAhAgAAACIAIAgAAC4AIAIAAAAiACAIAAAuACADAAAAGgAgDwAAJwAgEAAALAAgAQAAABoAIAEAAAAiACADFQAAlQEAIBgAAJcBACAZAACWAQAgCE4AAHwAME8AADUAEFAAAHwAMFEBAGgAIVhAAGsAIWcBAGgAIWgBAGgAIWlAAGsAIQMAAAAiACADAAA0ADAUAAA1ACADAAAAIgAgAwAAIwAwBAAAGgAgAQAAAB4AIAEAAAAeACADAAAAHAAgAwAAHQAwBAAAHgAgAwAAABwAIAMAAB0AMAQAAB4AIAMAAAAcACADAAAdADAEAAAeACAGHAAAlAEAIFEBAAAAAVhAAAAAAWQBAAAAAWUBAAAAAWYBAAAAAQEIAAA9ACAFUQEAAAABWEAAAAABZAEAAAABZQEAAAABZgEAAAABAQgAAD8AMAEIAAA_ADAGHAAAkwEAIFEBAIwBACFYQACPAQAhZAEAjAEAIWUBAIwBACFmAQCMAQAhAgAAAB4AIAgAAEIAIAVRAQCMAQAhWEAAjwEAIWQBAIwBACFlAQCMAQAhZgEAjAEAIQIAAAAcACAIAABEACACAAAAHAAgCAAARAAgAwAAAB4AIA8AAD0AIBAAAEIAIAEAAAAeACABAAAAHAAgAxUAAJABACAYAACSAQAgGQAAkQEAIAhOAAB7ADBPAABLABBQAAB7ADBRAQBoACFYQABrACFkAQBoACFlAQBoACFmAQBoACEDAAAAHAAgAwAASgAwFAAASwAgAwAAABwAIAMAAB0AMAQAAB4AIAtOAAB2ADBPAABRABBQAAB2ADBRAQAAAAFSAQAAAAFTAQB3ACFUQAB4ACFVAQB5ACFWQAB6ACFXQAB6ACFYQAB6ACEBAAAATgAgAQAAAE4AIAtOAAB2ADBPAABRABBQAAB2ADBRAQB3ACFSAQB3ACFTAQB3ACFUQAB4ACFVAQB5ACFWQAB6ACFXQAB6ACFYQAB6ACECVAAAiAEAIFUAAIgBACADAAAAUQAgAwAAUgAwBAAATgAgAwAAAFEAIAMAAFIAMAQAAE4AIAMAAABRACADAABSADAEAABOACAIUQEAAAABUgEAAAABUwEAAAABVEAAAAABVQEAAAABVkAAAAABV0AAAAABWEAAAAABAQgAAFYAIAhRAQAAAAFSAQAAAAFTAQAAAAFUQAAAAAFVAQAAAAFWQAAAAAFXQAAAAAFYQAAAAAEBCAAAWAAwAQgAAFgAMAhRAQCMAQAhUgEAjAEAIVMBAIwBACFUQACNAQAhVQEAjgEAIVZAAI8BACFXQACPAQAhWEAAjwEAIQIAAABOACAIAABbACAIUQEAjAEAIVIBAIwBACFTAQCMAQAhVEAAjQEAIVUBAI4BACFWQACPAQAhV0AAjwEAIVhAAI8BACECAAAAUQAgCAAAXQAgAgAAAFEAIAgAAF0AIAMAAABOACAPAABWACAQAABbACABAAAATgAgAQAAAFEAIAUVAACJAQAgGAAAiwEAIBkAAIoBACBUAACIAQAgVQAAiAEAIAtOAABnADBPAABkABBQAABnADBRAQBoACFSAQBoACFTAQBoACFUQABpACFVAQBqACFWQABrACFXQABrACFYQABrACEDAAAAUQAgAwAAYwAwFAAAZAAgAwAAAFEAIAMAAFIAMAQAAE4AIAtOAABnADBPAABkABBQAABnADBRAQBoACFSAQBoACFTAQBoACFUQABpACFVAQBqACFWQABrACFXQABrACFYQABrACEOFQAAbQAgGAAAdQAgGQAAdQAgWQEAAAABWgEAAAAEWwEAAAAEXAEAAAABXQEAAAABXgEAAAABXwEAAAABYAEAdAAhYQEAAAABYgEAAAABYwEAAAABCxUAAHAAIBgAAHMAIBkAAHMAIFlAAAAAAVpAAAAABVtAAAAABVxAAAAAAV1AAAAAAV5AAAAAAV9AAAAAAWBAAHIAIQ4VAABwACAYAABxACAZAABxACBZAQAAAAFaAQAAAAVbAQAAAAVcAQAAAAFdAQAAAAFeAQAAAAFfAQAAAAFgAQBvACFhAQAAAAFiAQAAAAFjAQAAAAELFQAAbQAgGAAAbgAgGQAAbgAgWUAAAAABWkAAAAAEW0AAAAAEXEAAAAABXUAAAAABXkAAAAABX0AAAAABYEAAbAAhCxUAAG0AIBgAAG4AIBkAAG4AIFlAAAAAAVpAAAAABFtAAAAABFxAAAAAAV1AAAAAAV5AAAAAAV9AAAAAAWBAAGwAIQhZAgAAAAFaAgAAAARbAgAAAARcAgAAAAFdAgAAAAFeAgAAAAFfAgAAAAFgAgBtACEIWUAAAAABWkAAAAAEW0AAAAAEXEAAAAABXUAAAAABXkAAAAABX0AAAAABYEAAbgAhDhUAAHAAIBgAAHEAIBkAAHEAIFkBAAAAAVoBAAAABVsBAAAABVwBAAAAAV0BAAAAAV4BAAAAAV8BAAAAAWABAG8AIWEBAAAAAWIBAAAAAWMBAAAAAQhZAgAAAAFaAgAAAAVbAgAAAAVcAgAAAAFdAgAAAAFeAgAAAAFfAgAAAAFgAgBwACELWQEAAAABWgEAAAAFWwEAAAAFXAEAAAABXQEAAAABXgEAAAABXwEAAAABYAEAcQAhYQEAAAABYgEAAAABYwEAAAABCxUAAHAAIBgAAHMAIBkAAHMAIFlAAAAAAVpAAAAABVtAAAAABVxAAAAAAV1AAAAAAV5AAAAAAV9AAAAAAWBAAHIAIQhZQAAAAAFaQAAAAAVbQAAAAAVcQAAAAAFdQAAAAAFeQAAAAAFfQAAAAAFgQABzACEOFQAAbQAgGAAAdQAgGQAAdQAgWQEAAAABWgEAAAAEWwEAAAAEXAEAAAABXQEAAAABXgEAAAABXwEAAAABYAEAdAAhYQEAAAABYgEAAAABYwEAAAABC1kBAAAAAVoBAAAABFsBAAAABFwBAAAAAV0BAAAAAV4BAAAAAV8BAAAAAWABAHUAIWEBAAAAAWIBAAAAAWMBAAAAAQtOAAB2ADBPAABRABBQAAB2ADBRAQB3ACFSAQB3ACFTAQB3ACFUQAB4ACFVAQB5ACFWQAB6ACFXQAB6ACFYQAB6ACELWQEAAAABWgEAAAAEWwEAAAAEXAEAAAABXQEAAAABXgEAAAABXwEAAAABYAEAdQAhYQEAAAABYgEAAAABYwEAAAABCFlAAAAAAVpAAAAABVtAAAAABVxAAAAAAV1AAAAAAV5AAAAAAV9AAAAAAWBAAHMAIQtZAQAAAAFaAQAAAAVbAQAAAAVcAQAAAAFdAQAAAAFeAQAAAAFfAQAAAAFgAQBxACFhAQAAAAFiAQAAAAFjAQAAAAEIWUAAAAABWkAAAAAEW0AAAAAEXEAAAAABXUAAAAABXkAAAAABX0AAAAABYEAAbgAhCE4AAHsAME8AAEsAEFAAAHsAMFEBAGgAIVhAAGsAIWQBAGgAIWUBAGgAIWYBAGgAIQhOAAB8ADBPAAA1ABBQAAB8ADBRAQBoACFYQABrACFnAQBoACFoAQBoACFpQABrACEJHQAAfgAgTgAAfQAwTwAAIgAQUAAAfQAwUQEAdwAhWEAAegAhZwEAdwAhaAEAdwAhaUAAegAhA2oAABwAIGsAABwAIGwAABwAIAJkAQAAAAFlAQAAAAEJHAAAgQEAIE4AAIABADBPAAAcABBQAACAAQAwUQEAdwAhWEAAegAhZAEAdwAhZQEAdwAhZgEAdwAhCx0AAH4AIE4AAH0AME8AACIAEFAAAH0AMFEBAHcAIVhAAHoAIWcBAHcAIWgBAHcAIWlAAHoAIW8AACIAIHAAACIAIAZOAACCAQAwTwAAFwAQUAAAggEAMFEBAGgAIWlAAGsAIW4CAIMBACENFQAAbQAgFgAAhQEAIBcAAG0AIBgAAG0AIBkAAG0AIFkCAAAAAVoCAAAABFsCAAAABFwCAAAAAV0CAAAAAV4CAAAAAV8CAAAAAWACAIQBACENFQAAbQAgFgAAhQEAIBcAAG0AIBgAAG0AIBkAAG0AIFkCAAAAAVoCAAAABFsCAAAABFwCAAAAAV0CAAAAAV4CAAAAAV8CAAAAAWACAIQBACEIWQgAAAABWggAAAAEWwgAAAAEXAgAAAABXQgAAAABXggAAAABXwgAAAABYAgAhQEAIQZOAACGAQAwTwAABAAQUAAAhgEAMFEBAHcAIWlAAHoAIW4CAIcBACEIWQIAAAABWgIAAAAEWwIAAAAEXAIAAAABXQIAAAABXgIAAAABXwIAAAABYAIAbQAhAAAAAAF0AQAAAAEBdEAAAAABAXQBAAAAAQF0QAAAAAEAAAAFDwAArwEAIBAAALIBACBxAACwAQAgcgAAsQEAIHcAABoAIAMPAACvAQAgcQAAsAEAIHcAABoAIAAAAAsPAACZAQAwEAAAngEAMHEAAJoBADByAACbAQAwcwAAnAEAIHQAAJ0BADB1AACdAQAwdgAAnQEAMHcAAJ0BADB4AACfAQAweQAAoAEAMARRAQAAAAFYQAAAAAFlAQAAAAFmAQAAAAECAAAAHgAgDwAApAEAIAMAAAAeACAPAACkAQAgEAAAowEAIAEIAACuAQAwChwAAIEBACBOAACAAQAwTwAAHAAQUAAAgAEAMFEBAAAAAVhAAHoAIWQBAHcAIWUBAHcAIWYBAHcAIW0AAH8AIAIAAAAeACAIAACjAQAgAgAAAKEBACAIAACiAQAgCE4AAKABADBPAAChAQAQUAAAoAEAMFEBAHcAIVhAAHoAIWQBAHcAIWUBAHcAIWYBAHcAIQhOAACgAQAwTwAAoQEAEFAAAKABADBRAQB3ACFYQAB6ACFkAQB3ACFlAQB3ACFmAQB3ACEEUQEAjAEAIVhAAI8BACFlAQCMAQAhZgEAjAEAIQRRAQCMAQAhWEAAjwEAIWUBAIwBACFmAQCMAQAhBFEBAAAAAVhAAAAAAWUBAAAAAWYBAAAAAQQPAACZAQAwcQAAmgEAMHMAAJwBACB3AACdAQAwAAEdAACmAQAgAAAAAAAFdAIAAAABegIAAAABewIAAAABfAIAAAABfQIAAAABBFEBAAAAAVhAAAAAAWUBAAAAAWYBAAAAAQVRAQAAAAFYQAAAAAFnAQAAAAFoAQAAAAFpQAAAAAECAAAAGgAgDwAArwEAIAMAAAAiACAPAACvAQAgEAAAswEAIAcAAAAiACAIAACzAQAgUQEAjAEAIVhAAI8BACFnAQCMAQAhaAEAjAEAIWlAAI8BACEFUQEAjAEAIVhAAI8BACFnAQCMAQAhaAEAjAEAIWlAAI8BACEAAAAABRUABhYABxcACBgACRkACgAAAAAABRUABhYABxcACBgACRkACgIVAA4dHw0BHAAMAR0gAAAAAxUAEhgAExkAFAAAAAMVABIYABMZABQBHAAMARwADAMVABkYABoZABsAAAADFQAZGAAaGQAbAAAAAxUAIRgAIhkAIwAAAAMVACEYACIZACMBAgECAwEFBgEGBwEHCAEJCgEKDAILDQMMDwENEQIOEgQREwESFAETFQIaGAUbGQseGwwfIQwgJAwhJQwiJgwjKAwkKgIlKw8mLQwnLwIoMBApMQwqMgwrMwIsNhEtNxUuOA0vOQ0wOg0xOw0yPA0zPg00QAI1QRY2Qw03RQI4Rhc5Rw06SA07SQI8TBg9TRw-Tx0_UB1AUx1BVB1CVR1DVx1EWQJFWh5GXB1HXgJIXx9JYB1KYR1LYgJMZSBNZiQ" } async function decodeBase64AsWasm(wasmBase64: string): Promise { @@ -227,16 +227,6 @@ export interface PrismaClient< * ``` */ get refreshToken(): Prisma.RefreshTokenDelegate; - - /** - * `prisma.uploadLog`: Exposes CRUD operations for the **UploadLog** model. - * Example usage: - * ```ts - * // Fetch zero or more UploadLogs - * const uploadLogs = await prisma.uploadLog.findMany() - * ``` - */ - get uploadLog(): Prisma.UploadLogDelegate; } export function getPrismaClientClass(): PrismaClientConstructor { diff --git a/apps/api/src/prisma/generated/prisma/internal/prismaNamespace.ts b/apps/api/src/prisma/generated/prisma/internal/prismaNamespace.ts index c7e9652..a5e767b 100644 --- a/apps/api/src/prisma/generated/prisma/internal/prismaNamespace.ts +++ b/apps/api/src/prisma/generated/prisma/internal/prismaNamespace.ts @@ -80,12 +80,12 @@ export type PrismaVersion = { } /** - * Prisma Client JS version: 7.9.1 - * Query Engine version: e922089b7d7502aff4249d5da3420f6fa55fc6ad + * Prisma Client JS version: 7.10.0 + * Query Engine version: 0edf323efd1d98336f3f0a68684b56f689b900d3 */ export const prismaVersion: PrismaVersion = { - client: "7.9.1", - engine: "e922089b7d7502aff4249d5da3420f6fa55fc6ad" + client: "7.10.0", + engine: "0edf323efd1d98336f3f0a68684b56f689b900d3" } /** @@ -400,8 +400,7 @@ export const ModelName = { AuthState: 'AuthState', DriveAccount: 'DriveAccount', AllowedFolder: 'AllowedFolder', - RefreshToken: 'RefreshToken', - UploadLog: 'UploadLog' + RefreshToken: 'RefreshToken' } as const export type ModelName = (typeof ModelName)[keyof typeof ModelName] @@ -417,7 +416,7 @@ export type TypeMap - fields: Prisma.UploadLogFieldRefs - operations: { - findUnique: { - args: Prisma.UploadLogFindUniqueArgs - result: runtime.Types.Utils.PayloadToResult | null - } - findUniqueOrThrow: { - args: Prisma.UploadLogFindUniqueOrThrowArgs - result: runtime.Types.Utils.PayloadToResult - } - findFirst: { - args: Prisma.UploadLogFindFirstArgs - result: runtime.Types.Utils.PayloadToResult | null - } - findFirstOrThrow: { - args: Prisma.UploadLogFindFirstOrThrowArgs - result: runtime.Types.Utils.PayloadToResult - } - findMany: { - args: Prisma.UploadLogFindManyArgs - result: runtime.Types.Utils.PayloadToResult[] - } - create: { - args: Prisma.UploadLogCreateArgs - result: runtime.Types.Utils.PayloadToResult - } - createMany: { - args: Prisma.UploadLogCreateManyArgs - result: BatchPayload - } - createManyAndReturn: { - args: Prisma.UploadLogCreateManyAndReturnArgs - result: runtime.Types.Utils.PayloadToResult[] - } - delete: { - args: Prisma.UploadLogDeleteArgs - result: runtime.Types.Utils.PayloadToResult - } - update: { - args: Prisma.UploadLogUpdateArgs - result: runtime.Types.Utils.PayloadToResult - } - deleteMany: { - args: Prisma.UploadLogDeleteManyArgs - result: BatchPayload - } - updateMany: { - args: Prisma.UploadLogUpdateManyArgs - result: BatchPayload - } - updateManyAndReturn: { - args: Prisma.UploadLogUpdateManyAndReturnArgs - result: runtime.Types.Utils.PayloadToResult[] - } - upsert: { - args: Prisma.UploadLogUpsertArgs - result: runtime.Types.Utils.PayloadToResult - } - aggregate: { - args: Prisma.UploadLogAggregateArgs - result: runtime.Types.Utils.Optional - } - groupBy: { - args: Prisma.UploadLogGroupByArgs - result: runtime.Types.Utils.Optional[] - } - count: { - args: Prisma.UploadLogCountArgs - result: runtime.Types.Utils.Optional | number - } - } - } } } & { other: { @@ -875,20 +800,6 @@ export const RefreshTokenScalarFieldEnum = { export type RefreshTokenScalarFieldEnum = (typeof RefreshTokenScalarFieldEnum)[keyof typeof RefreshTokenScalarFieldEnum] -export const UploadLogScalarFieldEnum = { - id: 'id', - fileName: 'fileName', - folderId: 'folderId', - fileId: 'fileId', - size: 'size', - status: 'status', - error: 'error', - createdAt: 'createdAt' -} as const - -export type UploadLogScalarFieldEnum = (typeof UploadLogScalarFieldEnum)[keyof typeof UploadLogScalarFieldEnum] - - export const SortOrder = { asc: 'asc', desc: 'desc' @@ -961,34 +872,6 @@ export type ListDateTimeFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaM -/** - * Reference to a field of type 'BigInt' - */ -export type BigIntFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'BigInt'> - - - -/** - * Reference to a field of type 'BigInt[]' - */ -export type ListBigIntFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'BigInt[]'> - - - -/** - * Reference to a field of type 'UploadStatus' - */ -export type EnumUploadStatusFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'UploadStatus'> - - - -/** - * Reference to a field of type 'UploadStatus[]' - */ -export type ListEnumUploadStatusFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'UploadStatus[]'> - - - /** * Reference to a field of type 'Float' */ @@ -1157,7 +1040,6 @@ export type GlobalOmitConfig = { driveAccount?: Prisma.DriveAccountOmit allowedFolder?: Prisma.AllowedFolderOmit refreshToken?: Prisma.RefreshTokenOmit - uploadLog?: Prisma.UploadLogOmit } /* Types for Logging */ diff --git a/apps/api/src/prisma/generated/prisma/internal/prismaNamespaceBrowser.ts b/apps/api/src/prisma/generated/prisma/internal/prismaNamespaceBrowser.ts index 2b63558..5afeaa1 100644 --- a/apps/api/src/prisma/generated/prisma/internal/prismaNamespaceBrowser.ts +++ b/apps/api/src/prisma/generated/prisma/internal/prismaNamespaceBrowser.ts @@ -54,8 +54,7 @@ export const ModelName = { AuthState: 'AuthState', DriveAccount: 'DriveAccount', AllowedFolder: 'AllowedFolder', - RefreshToken: 'RefreshToken', - UploadLog: 'UploadLog' + RefreshToken: 'RefreshToken' } as const export type ModelName = (typeof ModelName)[keyof typeof ModelName] @@ -119,20 +118,6 @@ export const RefreshTokenScalarFieldEnum = { export type RefreshTokenScalarFieldEnum = (typeof RefreshTokenScalarFieldEnum)[keyof typeof RefreshTokenScalarFieldEnum] -export const UploadLogScalarFieldEnum = { - id: 'id', - fileName: 'fileName', - folderId: 'folderId', - fileId: 'fileId', - size: 'size', - status: 'status', - error: 'error', - createdAt: 'createdAt' -} as const - -export type UploadLogScalarFieldEnum = (typeof UploadLogScalarFieldEnum)[keyof typeof UploadLogScalarFieldEnum] - - export const SortOrder = { asc: 'asc', desc: 'desc' diff --git a/apps/api/src/prisma/generated/prisma/models.ts b/apps/api/src/prisma/generated/prisma/models.ts index 2278e8c..33f99c8 100644 --- a/apps/api/src/prisma/generated/prisma/models.ts +++ b/apps/api/src/prisma/generated/prisma/models.ts @@ -12,5 +12,4 @@ export type * from './models/AuthState.js' export type * from './models/DriveAccount.js' export type * from './models/AllowedFolder.js' export type * from './models/RefreshToken.js' -export type * from './models/UploadLog.js' export type * from './commonInputTypes.js' \ No newline at end of file diff --git a/apps/api/src/prisma/generated/prisma/models/UploadLog.ts b/apps/api/src/prisma/generated/prisma/models/UploadLog.ts deleted file mode 100644 index beb6116..0000000 --- a/apps/api/src/prisma/generated/prisma/models/UploadLog.ts +++ /dev/null @@ -1,1289 +0,0 @@ - -/* !!! This is code generated by Prisma. Do not edit directly. !!! */ -/* eslint-disable */ -// biome-ignore-all lint: generated file -// @ts-nocheck -/* - * This file exports the `UploadLog` model and its related types. - * - * 🟢 You can import this file directly. - */ -import type * as runtime from "@prisma/client/runtime/client" -import type * as $Enums from "../enums.js" -import type * as Prisma from "../internal/prismaNamespace.js" - -/** - * Model UploadLog - * Audit trail of upload attempts for the history view. - */ -export type UploadLogModel = runtime.Types.Result.DefaultSelection - -export type AggregateUploadLog = { - _count: UploadLogCountAggregateOutputType | null - _avg: UploadLogAvgAggregateOutputType | null - _sum: UploadLogSumAggregateOutputType | null - _min: UploadLogMinAggregateOutputType | null - _max: UploadLogMaxAggregateOutputType | null -} - -export type UploadLogAvgAggregateOutputType = { - size: number | null -} - -export type UploadLogSumAggregateOutputType = { - size: bigint | null -} - -export type UploadLogMinAggregateOutputType = { - id: string | null - fileName: string | null - folderId: string | null - fileId: string | null - size: bigint | null - status: $Enums.UploadStatus | null - error: string | null - createdAt: Date | null -} - -export type UploadLogMaxAggregateOutputType = { - id: string | null - fileName: string | null - folderId: string | null - fileId: string | null - size: bigint | null - status: $Enums.UploadStatus | null - error: string | null - createdAt: Date | null -} - -export type UploadLogCountAggregateOutputType = { - id: number - fileName: number - folderId: number - fileId: number - size: number - status: number - error: number - createdAt: number - _all: number -} - - -export type UploadLogAvgAggregateInputType = { - size?: true -} - -export type UploadLogSumAggregateInputType = { - size?: true -} - -export type UploadLogMinAggregateInputType = { - id?: true - fileName?: true - folderId?: true - fileId?: true - size?: true - status?: true - error?: true - createdAt?: true -} - -export type UploadLogMaxAggregateInputType = { - id?: true - fileName?: true - folderId?: true - fileId?: true - size?: true - status?: true - error?: true - createdAt?: true -} - -export type UploadLogCountAggregateInputType = { - id?: true - fileName?: true - folderId?: true - fileId?: true - size?: true - status?: true - error?: true - createdAt?: true - _all?: true -} - -export type UploadLogAggregateArgs = { - /** - * Filter which UploadLog to aggregate. - */ - where?: Prisma.UploadLogWhereInput - /** - * {@link https://www.prisma.io/docs/concepts/components/prisma-client/sorting Sorting Docs} - * - * Determine the order of UploadLogs to fetch. - */ - orderBy?: Prisma.UploadLogOrderByWithRelationInput | Prisma.UploadLogOrderByWithRelationInput[] - /** - * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination#cursor-based-pagination Cursor Docs} - * - * Sets the start position - */ - cursor?: Prisma.UploadLogWhereUniqueInput - /** - * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs} - * - * Take `±n` UploadLogs from the position of the cursor. - */ - take?: number - /** - * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs} - * - * Skip the first `n` UploadLogs. - */ - skip?: number - /** - * {@link https://www.prisma.io/docs/concepts/components/prisma-client/aggregations Aggregation Docs} - * - * Count returned UploadLogs - **/ - _count?: true | UploadLogCountAggregateInputType - /** - * {@link https://www.prisma.io/docs/concepts/components/prisma-client/aggregations Aggregation Docs} - * - * Select which fields to average - **/ - _avg?: UploadLogAvgAggregateInputType - /** - * {@link https://www.prisma.io/docs/concepts/components/prisma-client/aggregations Aggregation Docs} - * - * Select which fields to sum - **/ - _sum?: UploadLogSumAggregateInputType - /** - * {@link https://www.prisma.io/docs/concepts/components/prisma-client/aggregations Aggregation Docs} - * - * Select which fields to find the minimum value - **/ - _min?: UploadLogMinAggregateInputType - /** - * {@link https://www.prisma.io/docs/concepts/components/prisma-client/aggregations Aggregation Docs} - * - * Select which fields to find the maximum value - **/ - _max?: UploadLogMaxAggregateInputType -} - -export type GetUploadLogAggregateType = { - [P in keyof T & keyof AggregateUploadLog]: P extends '_count' | 'count' - ? T[P] extends true - ? number - : Prisma.GetScalarType - : Prisma.GetScalarType -} - - - - -export type UploadLogGroupByArgs = { - where?: Prisma.UploadLogWhereInput - orderBy?: Prisma.UploadLogOrderByWithAggregationInput | Prisma.UploadLogOrderByWithAggregationInput[] - by: Prisma.UploadLogScalarFieldEnum[] | Prisma.UploadLogScalarFieldEnum - having?: Prisma.UploadLogScalarWhereWithAggregatesInput - take?: number - skip?: number - _count?: UploadLogCountAggregateInputType | true - _avg?: UploadLogAvgAggregateInputType - _sum?: UploadLogSumAggregateInputType - _min?: UploadLogMinAggregateInputType - _max?: UploadLogMaxAggregateInputType -} - -export type UploadLogGroupByOutputType = { - id: string - fileName: string - folderId: string - fileId: string | null - size: bigint | null - status: $Enums.UploadStatus - error: string | null - createdAt: Date - _count: UploadLogCountAggregateOutputType | null - _avg: UploadLogAvgAggregateOutputType | null - _sum: UploadLogSumAggregateOutputType | null - _min: UploadLogMinAggregateOutputType | null - _max: UploadLogMaxAggregateOutputType | null -} - -export type GetUploadLogGroupByPayload = Prisma.PrismaPromise< - Array< - Prisma.PickEnumerable & - { - [P in ((keyof T) & (keyof UploadLogGroupByOutputType))]: P extends '_count' - ? T[P] extends boolean - ? number - : Prisma.GetScalarType - : Prisma.GetScalarType - } - > - > - - - -export type UploadLogWhereInput = { - AND?: Prisma.UploadLogWhereInput | Prisma.UploadLogWhereInput[] - OR?: Prisma.UploadLogWhereInput[] - NOT?: Prisma.UploadLogWhereInput | Prisma.UploadLogWhereInput[] - id?: Prisma.StringFilter<"UploadLog"> | string - fileName?: Prisma.StringFilter<"UploadLog"> | string - folderId?: Prisma.StringFilter<"UploadLog"> | string - fileId?: Prisma.StringNullableFilter<"UploadLog"> | string | null - size?: Prisma.BigIntNullableFilter<"UploadLog"> | bigint | number | null - status?: Prisma.EnumUploadStatusFilter<"UploadLog"> | $Enums.UploadStatus - error?: Prisma.StringNullableFilter<"UploadLog"> | string | null - createdAt?: Prisma.DateTimeFilter<"UploadLog"> | Date | string -} - -export type UploadLogOrderByWithRelationInput = { - id?: Prisma.SortOrder - fileName?: Prisma.SortOrder - folderId?: Prisma.SortOrder - fileId?: Prisma.SortOrderInput | Prisma.SortOrder - size?: Prisma.SortOrderInput | Prisma.SortOrder - status?: Prisma.SortOrder - error?: Prisma.SortOrderInput | Prisma.SortOrder - createdAt?: Prisma.SortOrder -} - -export type UploadLogWhereUniqueInput = Prisma.AtLeast<{ - id?: string - AND?: Prisma.UploadLogWhereInput | Prisma.UploadLogWhereInput[] - OR?: Prisma.UploadLogWhereInput[] - NOT?: Prisma.UploadLogWhereInput | Prisma.UploadLogWhereInput[] - fileName?: Prisma.StringFilter<"UploadLog"> | string - folderId?: Prisma.StringFilter<"UploadLog"> | string - fileId?: Prisma.StringNullableFilter<"UploadLog"> | string | null - size?: Prisma.BigIntNullableFilter<"UploadLog"> | bigint | number | null - status?: Prisma.EnumUploadStatusFilter<"UploadLog"> | $Enums.UploadStatus - error?: Prisma.StringNullableFilter<"UploadLog"> | string | null - createdAt?: Prisma.DateTimeFilter<"UploadLog"> | Date | string -}, "id"> - -export type UploadLogOrderByWithAggregationInput = { - id?: Prisma.SortOrder - fileName?: Prisma.SortOrder - folderId?: Prisma.SortOrder - fileId?: Prisma.SortOrderInput | Prisma.SortOrder - size?: Prisma.SortOrderInput | Prisma.SortOrder - status?: Prisma.SortOrder - error?: Prisma.SortOrderInput | Prisma.SortOrder - createdAt?: Prisma.SortOrder - _count?: Prisma.UploadLogCountOrderByAggregateInput - _avg?: Prisma.UploadLogAvgOrderByAggregateInput - _max?: Prisma.UploadLogMaxOrderByAggregateInput - _min?: Prisma.UploadLogMinOrderByAggregateInput - _sum?: Prisma.UploadLogSumOrderByAggregateInput -} - -export type UploadLogScalarWhereWithAggregatesInput = { - AND?: Prisma.UploadLogScalarWhereWithAggregatesInput | Prisma.UploadLogScalarWhereWithAggregatesInput[] - OR?: Prisma.UploadLogScalarWhereWithAggregatesInput[] - NOT?: Prisma.UploadLogScalarWhereWithAggregatesInput | Prisma.UploadLogScalarWhereWithAggregatesInput[] - id?: Prisma.StringWithAggregatesFilter<"UploadLog"> | string - fileName?: Prisma.StringWithAggregatesFilter<"UploadLog"> | string - folderId?: Prisma.StringWithAggregatesFilter<"UploadLog"> | string - fileId?: Prisma.StringNullableWithAggregatesFilter<"UploadLog"> | string | null - size?: Prisma.BigIntNullableWithAggregatesFilter<"UploadLog"> | bigint | number | null - status?: Prisma.EnumUploadStatusWithAggregatesFilter<"UploadLog"> | $Enums.UploadStatus - error?: Prisma.StringNullableWithAggregatesFilter<"UploadLog"> | string | null - createdAt?: Prisma.DateTimeWithAggregatesFilter<"UploadLog"> | Date | string -} - -export type UploadLogCreateInput = { - id?: string - fileName: string - folderId: string - fileId?: string | null - size?: bigint | number | null - status: $Enums.UploadStatus - error?: string | null - createdAt?: Date | string -} - -export type UploadLogUncheckedCreateInput = { - id?: string - fileName: string - folderId: string - fileId?: string | null - size?: bigint | number | null - status: $Enums.UploadStatus - error?: string | null - createdAt?: Date | string -} - -export type UploadLogUpdateInput = { - id?: Prisma.StringFieldUpdateOperationsInput | string - fileName?: Prisma.StringFieldUpdateOperationsInput | string - folderId?: Prisma.StringFieldUpdateOperationsInput | string - fileId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null - size?: Prisma.NullableBigIntFieldUpdateOperationsInput | bigint | number | null - status?: Prisma.EnumUploadStatusFieldUpdateOperationsInput | $Enums.UploadStatus - error?: Prisma.NullableStringFieldUpdateOperationsInput | string | null - createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string -} - -export type UploadLogUncheckedUpdateInput = { - id?: Prisma.StringFieldUpdateOperationsInput | string - fileName?: Prisma.StringFieldUpdateOperationsInput | string - folderId?: Prisma.StringFieldUpdateOperationsInput | string - fileId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null - size?: Prisma.NullableBigIntFieldUpdateOperationsInput | bigint | number | null - status?: Prisma.EnumUploadStatusFieldUpdateOperationsInput | $Enums.UploadStatus - error?: Prisma.NullableStringFieldUpdateOperationsInput | string | null - createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string -} - -export type UploadLogCreateManyInput = { - id?: string - fileName: string - folderId: string - fileId?: string | null - size?: bigint | number | null - status: $Enums.UploadStatus - error?: string | null - createdAt?: Date | string -} - -export type UploadLogUpdateManyMutationInput = { - id?: Prisma.StringFieldUpdateOperationsInput | string - fileName?: Prisma.StringFieldUpdateOperationsInput | string - folderId?: Prisma.StringFieldUpdateOperationsInput | string - fileId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null - size?: Prisma.NullableBigIntFieldUpdateOperationsInput | bigint | number | null - status?: Prisma.EnumUploadStatusFieldUpdateOperationsInput | $Enums.UploadStatus - error?: Prisma.NullableStringFieldUpdateOperationsInput | string | null - createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string -} - -export type UploadLogUncheckedUpdateManyInput = { - id?: Prisma.StringFieldUpdateOperationsInput | string - fileName?: Prisma.StringFieldUpdateOperationsInput | string - folderId?: Prisma.StringFieldUpdateOperationsInput | string - fileId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null - size?: Prisma.NullableBigIntFieldUpdateOperationsInput | bigint | number | null - status?: Prisma.EnumUploadStatusFieldUpdateOperationsInput | $Enums.UploadStatus - error?: Prisma.NullableStringFieldUpdateOperationsInput | string | null - createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string -} - -export type UploadLogCountOrderByAggregateInput = { - id?: Prisma.SortOrder - fileName?: Prisma.SortOrder - folderId?: Prisma.SortOrder - fileId?: Prisma.SortOrder - size?: Prisma.SortOrder - status?: Prisma.SortOrder - error?: Prisma.SortOrder - createdAt?: Prisma.SortOrder -} - -export type UploadLogAvgOrderByAggregateInput = { - size?: Prisma.SortOrder -} - -export type UploadLogMaxOrderByAggregateInput = { - id?: Prisma.SortOrder - fileName?: Prisma.SortOrder - folderId?: Prisma.SortOrder - fileId?: Prisma.SortOrder - size?: Prisma.SortOrder - status?: Prisma.SortOrder - error?: Prisma.SortOrder - createdAt?: Prisma.SortOrder -} - -export type UploadLogMinOrderByAggregateInput = { - id?: Prisma.SortOrder - fileName?: Prisma.SortOrder - folderId?: Prisma.SortOrder - fileId?: Prisma.SortOrder - size?: Prisma.SortOrder - status?: Prisma.SortOrder - error?: Prisma.SortOrder - createdAt?: Prisma.SortOrder -} - -export type UploadLogSumOrderByAggregateInput = { - size?: Prisma.SortOrder -} - -export type NullableBigIntFieldUpdateOperationsInput = { - set?: bigint | number | null - increment?: bigint | number - decrement?: bigint | number - multiply?: bigint | number - divide?: bigint | number -} - -export type EnumUploadStatusFieldUpdateOperationsInput = { - set?: $Enums.UploadStatus -} - - - -export type UploadLogSelect = runtime.Types.Extensions.GetSelect<{ - id?: boolean - fileName?: boolean - folderId?: boolean - fileId?: boolean - size?: boolean - status?: boolean - error?: boolean - createdAt?: boolean -}, ExtArgs["result"]["uploadLog"]> - -export type UploadLogSelectCreateManyAndReturn = runtime.Types.Extensions.GetSelect<{ - id?: boolean - fileName?: boolean - folderId?: boolean - fileId?: boolean - size?: boolean - status?: boolean - error?: boolean - createdAt?: boolean -}, ExtArgs["result"]["uploadLog"]> - -export type UploadLogSelectUpdateManyAndReturn = runtime.Types.Extensions.GetSelect<{ - id?: boolean - fileName?: boolean - folderId?: boolean - fileId?: boolean - size?: boolean - status?: boolean - error?: boolean - createdAt?: boolean -}, ExtArgs["result"]["uploadLog"]> - -export type UploadLogSelectScalar = { - id?: boolean - fileName?: boolean - folderId?: boolean - fileId?: boolean - size?: boolean - status?: boolean - error?: boolean - createdAt?: boolean -} - -export type UploadLogOmit = runtime.Types.Extensions.GetOmit<"id" | "fileName" | "folderId" | "fileId" | "size" | "status" | "error" | "createdAt", ExtArgs["result"]["uploadLog"]> - -export type $UploadLogPayload = { - name: "UploadLog" - objects: {} - scalars: runtime.Types.Extensions.GetPayloadResult<{ - id: string - fileName: string - folderId: string - fileId: string | null - size: bigint | null - status: $Enums.UploadStatus - error: string | null - createdAt: Date - }, ExtArgs["result"]["uploadLog"]> - composites: {} -} - -export type UploadLogGetPayload = runtime.Types.Result.GetResult - -export type UploadLogCountArgs = - Omit & { - select?: UploadLogCountAggregateInputType | true - } - -export interface UploadLogDelegate { - [K: symbol]: { types: Prisma.TypeMap['model']['UploadLog'], meta: { name: 'UploadLog' } } - /** - * Find zero or one UploadLog that matches the filter. - * @param {UploadLogFindUniqueArgs} args - Arguments to find a UploadLog - * @example - * // Get one UploadLog - * const uploadLog = await prisma.uploadLog.findUnique({ - * where: { - * // ... provide filter here - * } - * }) - */ - findUnique(args: Prisma.SelectSubset>): Prisma.Prisma__UploadLogClient, T, "findUnique", GlobalOmitOptions> | null, null, ExtArgs, GlobalOmitOptions> - - /** - * Find one UploadLog that matches the filter or throw an error with `error.code='P2025'` - * if no matches were found. - * @param {UploadLogFindUniqueOrThrowArgs} args - Arguments to find a UploadLog - * @example - * // Get one UploadLog - * const uploadLog = await prisma.uploadLog.findUniqueOrThrow({ - * where: { - * // ... provide filter here - * } - * }) - */ - findUniqueOrThrow(args: Prisma.SelectSubset>): Prisma.Prisma__UploadLogClient, T, "findUniqueOrThrow", GlobalOmitOptions>, never, ExtArgs, GlobalOmitOptions> - - /** - * Find the first UploadLog that matches the filter. - * Note, that providing `undefined` is treated as the value not being there. - * Read more here: https://pris.ly/d/null-undefined - * @param {UploadLogFindFirstArgs} args - Arguments to find a UploadLog - * @example - * // Get one UploadLog - * const uploadLog = await prisma.uploadLog.findFirst({ - * where: { - * // ... provide filter here - * } - * }) - */ - findFirst(args?: Prisma.SelectSubset>): Prisma.Prisma__UploadLogClient, T, "findFirst", GlobalOmitOptions> | null, null, ExtArgs, GlobalOmitOptions> - - /** - * Find the first UploadLog that matches the filter or - * throw `PrismaKnownClientError` with `P2025` code if no matches were found. - * Note, that providing `undefined` is treated as the value not being there. - * Read more here: https://pris.ly/d/null-undefined - * @param {UploadLogFindFirstOrThrowArgs} args - Arguments to find a UploadLog - * @example - * // Get one UploadLog - * const uploadLog = await prisma.uploadLog.findFirstOrThrow({ - * where: { - * // ... provide filter here - * } - * }) - */ - findFirstOrThrow(args?: Prisma.SelectSubset>): Prisma.Prisma__UploadLogClient, T, "findFirstOrThrow", GlobalOmitOptions>, never, ExtArgs, GlobalOmitOptions> - - /** - * Find zero or more UploadLogs that matches the filter. - * Note, that providing `undefined` is treated as the value not being there. - * Read more here: https://pris.ly/d/null-undefined - * @param {UploadLogFindManyArgs} args - Arguments to filter and select certain fields only. - * @example - * // Get all UploadLogs - * const uploadLogs = await prisma.uploadLog.findMany() - * - * // Get first 10 UploadLogs - * const uploadLogs = await prisma.uploadLog.findMany({ take: 10 }) - * - * // Only select the `id` - * const uploadLogWithIdOnly = await prisma.uploadLog.findMany({ select: { id: true } }) - * - */ - findMany(args?: Prisma.SelectSubset>): Prisma.PrismaPromise, T, "findMany", GlobalOmitOptions>> - - /** - * Create a UploadLog. - * @param {UploadLogCreateArgs} args - Arguments to create a UploadLog. - * @example - * // Create one UploadLog - * const UploadLog = await prisma.uploadLog.create({ - * data: { - * // ... data to create a UploadLog - * } - * }) - * - */ - create(args: Prisma.SelectSubset>): Prisma.Prisma__UploadLogClient, T, "create", GlobalOmitOptions>, never, ExtArgs, GlobalOmitOptions> - - /** - * Create many UploadLogs. - * @param {UploadLogCreateManyArgs} args - Arguments to create many UploadLogs. - * @example - * // Create many UploadLogs - * const uploadLog = await prisma.uploadLog.createMany({ - * data: [ - * // ... provide data here - * ] - * }) - * - */ - createMany(args?: Prisma.SelectSubset>): Prisma.PrismaPromise - - /** - * Create many UploadLogs and returns the data saved in the database. - * @param {UploadLogCreateManyAndReturnArgs} args - Arguments to create many UploadLogs. - * @example - * // Create many UploadLogs - * const uploadLog = await prisma.uploadLog.createManyAndReturn({ - * data: [ - * // ... provide data here - * ] - * }) - * - * // Create many UploadLogs and only return the `id` - * const uploadLogWithIdOnly = await prisma.uploadLog.createManyAndReturn({ - * select: { id: true }, - * data: [ - * // ... provide data here - * ] - * }) - * Note, that providing `undefined` is treated as the value not being there. - * Read more here: https://pris.ly/d/null-undefined - * - */ - createManyAndReturn(args?: Prisma.SelectSubset>): Prisma.PrismaPromise, T, "createManyAndReturn", GlobalOmitOptions>> - - /** - * Delete a UploadLog. - * @param {UploadLogDeleteArgs} args - Arguments to delete one UploadLog. - * @example - * // Delete one UploadLog - * const UploadLog = await prisma.uploadLog.delete({ - * where: { - * // ... filter to delete one UploadLog - * } - * }) - * - */ - delete(args: Prisma.SelectSubset>): Prisma.Prisma__UploadLogClient, T, "delete", GlobalOmitOptions>, never, ExtArgs, GlobalOmitOptions> - - /** - * Update one UploadLog. - * @param {UploadLogUpdateArgs} args - Arguments to update one UploadLog. - * @example - * // Update one UploadLog - * const uploadLog = await prisma.uploadLog.update({ - * where: { - * // ... provide filter here - * }, - * data: { - * // ... provide data here - * } - * }) - * - */ - update(args: Prisma.SelectSubset>): Prisma.Prisma__UploadLogClient, T, "update", GlobalOmitOptions>, never, ExtArgs, GlobalOmitOptions> - - /** - * Delete zero or more UploadLogs. - * @param {UploadLogDeleteManyArgs} args - Arguments to filter UploadLogs to delete. - * @example - * // Delete a few UploadLogs - * const { count } = await prisma.uploadLog.deleteMany({ - * where: { - * // ... provide filter here - * } - * }) - * - */ - deleteMany(args?: Prisma.SelectSubset>): Prisma.PrismaPromise - - /** - * Update zero or more UploadLogs. - * Note, that providing `undefined` is treated as the value not being there. - * Read more here: https://pris.ly/d/null-undefined - * @param {UploadLogUpdateManyArgs} args - Arguments to update one or more rows. - * @example - * // Update many UploadLogs - * const uploadLog = await prisma.uploadLog.updateMany({ - * where: { - * // ... provide filter here - * }, - * data: { - * // ... provide data here - * } - * }) - * - */ - updateMany(args: Prisma.SelectSubset>): Prisma.PrismaPromise - - /** - * Update zero or more UploadLogs and returns the data updated in the database. - * @param {UploadLogUpdateManyAndReturnArgs} args - Arguments to update many UploadLogs. - * @example - * // Update many UploadLogs - * const uploadLog = await prisma.uploadLog.updateManyAndReturn({ - * where: { - * // ... provide filter here - * }, - * data: [ - * // ... provide data here - * ] - * }) - * - * // Update zero or more UploadLogs and only return the `id` - * const uploadLogWithIdOnly = await prisma.uploadLog.updateManyAndReturn({ - * select: { id: true }, - * where: { - * // ... provide filter here - * }, - * data: [ - * // ... provide data here - * ] - * }) - * Note, that providing `undefined` is treated as the value not being there. - * Read more here: https://pris.ly/d/null-undefined - * - */ - updateManyAndReturn(args: Prisma.SelectSubset>): Prisma.PrismaPromise, T, "updateManyAndReturn", GlobalOmitOptions>> - - /** - * Create or update one UploadLog. - * @param {UploadLogUpsertArgs} args - Arguments to update or create a UploadLog. - * @example - * // Update or create a UploadLog - * const uploadLog = await prisma.uploadLog.upsert({ - * create: { - * // ... data to create a UploadLog - * }, - * update: { - * // ... in case it already exists, update - * }, - * where: { - * // ... the filter for the UploadLog we want to update - * } - * }) - */ - upsert(args: Prisma.SelectSubset>): Prisma.Prisma__UploadLogClient, T, "upsert", GlobalOmitOptions>, never, ExtArgs, GlobalOmitOptions> - - - /** - * Count the number of UploadLogs. - * Note, that providing `undefined` is treated as the value not being there. - * Read more here: https://pris.ly/d/null-undefined - * @param {UploadLogCountArgs} args - Arguments to filter UploadLogs to count. - * @example - * // Count the number of UploadLogs - * const count = await prisma.uploadLog.count({ - * where: { - * // ... the filter for the UploadLogs we want to count - * } - * }) - **/ - count( - args?: Prisma.Subset, - ): Prisma.PrismaPromise< - T extends runtime.Types.Utils.Record<'select', any> - ? T['select'] extends true - ? number - : Prisma.GetScalarType - : number - > - - /** - * Allows you to perform aggregations operations on a UploadLog. - * Note, that providing `undefined` is treated as the value not being there. - * Read more here: https://pris.ly/d/null-undefined - * @param {UploadLogAggregateArgs} args - Select which aggregations you would like to apply and on what fields. - * @example - * // Ordered by age ascending - * // Where email contains prisma.io - * // Limited to the 10 users - * const aggregations = await prisma.user.aggregate({ - * _avg: { - * age: true, - * }, - * where: { - * email: { - * contains: "prisma.io", - * }, - * }, - * orderBy: { - * age: "asc", - * }, - * take: 10, - * }) - **/ - aggregate(args: Prisma.Subset): Prisma.PrismaPromise> - - /** - * Group by UploadLog. - * Note, that providing `undefined` is treated as the value not being there. - * Read more here: https://pris.ly/d/null-undefined - * @param {UploadLogGroupByArgs} args - Group by arguments. - * @example - * // Group by city, order by createdAt, get count - * const result = await prisma.user.groupBy({ - * by: ['city', 'createdAt'], - * orderBy: { - * createdAt: true - * }, - * _count: { - * _all: true - * }, - * }) - * - **/ - groupBy< - T extends UploadLogGroupByArgs, - HasSelectOrTake extends Prisma.Or< - Prisma.Extends<'skip', Prisma.Keys>, - Prisma.Extends<'take', Prisma.Keys> - >, - OrderByArg extends Prisma.True extends HasSelectOrTake - ? { orderBy: UploadLogGroupByArgs['orderBy'] } - : { orderBy?: UploadLogGroupByArgs['orderBy'] }, - OrderFields extends Prisma.ExcludeUnderscoreKeys>>, - ByFields extends Prisma.MaybeTupleToUnion, - ByValid extends Prisma.Has, - HavingFields extends Prisma.GetHavingFields, - HavingValid extends Prisma.Has, - ByEmpty extends T['by'] extends never[] ? Prisma.True : Prisma.False, - InputErrors extends ByEmpty extends Prisma.True - ? `Error: "by" must not be empty.` - : HavingValid extends Prisma.False - ? { - [P in HavingFields]: P extends ByFields - ? never - : P extends string - ? `Error: Field "${P}" used in "having" needs to be provided in "by".` - : [ - Error, - 'Field ', - P, - ` in "having" needs to be provided in "by"`, - ] - }[HavingFields] - : 'take' extends Prisma.Keys - ? 'orderBy' extends Prisma.Keys - ? ByValid extends Prisma.True - ? {} - : { - [P in OrderFields]: P extends ByFields - ? never - : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` - }[OrderFields] - : 'Error: If you provide "take", you also need to provide "orderBy"' - : 'skip' extends Prisma.Keys - ? 'orderBy' extends Prisma.Keys - ? ByValid extends Prisma.True - ? {} - : { - [P in OrderFields]: P extends ByFields - ? never - : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` - }[OrderFields] - : 'Error: If you provide "skip", you also need to provide "orderBy"' - : ByValid extends Prisma.True - ? {} - : { - [P in OrderFields]: P extends ByFields - ? never - : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` - }[OrderFields] - >(args: Prisma.SubsetIntersection & InputErrors): {} extends InputErrors ? GetUploadLogGroupByPayload : Prisma.PrismaPromise -/** - * Fields of the UploadLog model - */ -readonly fields: UploadLogFieldRefs; -} - -/** - * The delegate class that acts as a "Promise-like" for UploadLog. - * Why is this prefixed with `Prisma__`? - * Because we want to prevent naming conflicts as mentioned in - * https://github.com/prisma/prisma-client-js/issues/707 - */ -export interface Prisma__UploadLogClient extends Prisma.PrismaPromise { - readonly [Symbol.toStringTag]: "PrismaPromise" - /** - * Attaches callbacks for the resolution and/or rejection of the Promise. - * @param onfulfilled The callback to execute when the Promise is resolved. - * @param onrejected The callback to execute when the Promise is rejected. - * @returns A Promise for the completion of which ever callback is executed. - */ - then(onfulfilled?: ((value: T) => TResult1 | PromiseLike) | undefined | null, onrejected?: ((reason: any) => TResult2 | PromiseLike) | undefined | null): runtime.Types.Utils.JsPromise - /** - * Attaches a callback for only the rejection of the Promise. - * @param onrejected The callback to execute when the Promise is rejected. - * @returns A Promise for the completion of the callback. - */ - catch(onrejected?: ((reason: any) => TResult | PromiseLike) | undefined | null): runtime.Types.Utils.JsPromise - /** - * Attaches a callback that is invoked when the Promise is settled (fulfilled or rejected). The - * resolved value cannot be modified from the callback. - * @param onfinally The callback to execute when the Promise is settled (fulfilled or rejected). - * @returns A Promise for the completion of the callback. - */ - finally(onfinally?: (() => void) | undefined | null): runtime.Types.Utils.JsPromise -} - - - - -/** - * Fields of the UploadLog model - */ -export interface UploadLogFieldRefs { - readonly id: Prisma.FieldRef<"UploadLog", 'String'> - readonly fileName: Prisma.FieldRef<"UploadLog", 'String'> - readonly folderId: Prisma.FieldRef<"UploadLog", 'String'> - readonly fileId: Prisma.FieldRef<"UploadLog", 'String'> - readonly size: Prisma.FieldRef<"UploadLog", 'BigInt'> - readonly status: Prisma.FieldRef<"UploadLog", 'UploadStatus'> - readonly error: Prisma.FieldRef<"UploadLog", 'String'> - readonly createdAt: Prisma.FieldRef<"UploadLog", 'DateTime'> -} - - -// Custom InputTypes -/** - * UploadLog findUnique - */ -export type UploadLogFindUniqueArgs = { - /** - * Select specific fields to fetch from the UploadLog - */ - select?: Prisma.UploadLogSelect | null - /** - * Omit specific fields from the UploadLog - */ - omit?: Prisma.UploadLogOmit | null - /** - * Filter, which UploadLog to fetch. - */ - where: Prisma.UploadLogWhereUniqueInput -} - -/** - * UploadLog findUniqueOrThrow - */ -export type UploadLogFindUniqueOrThrowArgs = { - /** - * Select specific fields to fetch from the UploadLog - */ - select?: Prisma.UploadLogSelect | null - /** - * Omit specific fields from the UploadLog - */ - omit?: Prisma.UploadLogOmit | null - /** - * Filter, which UploadLog to fetch. - */ - where: Prisma.UploadLogWhereUniqueInput -} - -/** - * UploadLog findFirst - */ -export type UploadLogFindFirstArgs = { - /** - * Select specific fields to fetch from the UploadLog - */ - select?: Prisma.UploadLogSelect | null - /** - * Omit specific fields from the UploadLog - */ - omit?: Prisma.UploadLogOmit | null - /** - * Filter, which UploadLog to fetch. - */ - where?: Prisma.UploadLogWhereInput - /** - * {@link https://www.prisma.io/docs/concepts/components/prisma-client/sorting Sorting Docs} - * - * Determine the order of UploadLogs to fetch. - */ - orderBy?: Prisma.UploadLogOrderByWithRelationInput | Prisma.UploadLogOrderByWithRelationInput[] - /** - * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination#cursor-based-pagination Cursor Docs} - * - * Sets the position for searching for UploadLogs. - */ - cursor?: Prisma.UploadLogWhereUniqueInput - /** - * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs} - * - * Take `±n` UploadLogs from the position of the cursor. - */ - take?: number - /** - * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs} - * - * Skip the first `n` UploadLogs. - */ - skip?: number - /** - * {@link https://www.prisma.io/docs/concepts/components/prisma-client/distinct Distinct Docs} - * - * Filter by unique combinations of UploadLogs. - */ - distinct?: Prisma.UploadLogScalarFieldEnum | Prisma.UploadLogScalarFieldEnum[] -} - -/** - * UploadLog findFirstOrThrow - */ -export type UploadLogFindFirstOrThrowArgs = { - /** - * Select specific fields to fetch from the UploadLog - */ - select?: Prisma.UploadLogSelect | null - /** - * Omit specific fields from the UploadLog - */ - omit?: Prisma.UploadLogOmit | null - /** - * Filter, which UploadLog to fetch. - */ - where?: Prisma.UploadLogWhereInput - /** - * {@link https://www.prisma.io/docs/concepts/components/prisma-client/sorting Sorting Docs} - * - * Determine the order of UploadLogs to fetch. - */ - orderBy?: Prisma.UploadLogOrderByWithRelationInput | Prisma.UploadLogOrderByWithRelationInput[] - /** - * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination#cursor-based-pagination Cursor Docs} - * - * Sets the position for searching for UploadLogs. - */ - cursor?: Prisma.UploadLogWhereUniqueInput - /** - * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs} - * - * Take `±n` UploadLogs from the position of the cursor. - */ - take?: number - /** - * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs} - * - * Skip the first `n` UploadLogs. - */ - skip?: number - /** - * {@link https://www.prisma.io/docs/concepts/components/prisma-client/distinct Distinct Docs} - * - * Filter by unique combinations of UploadLogs. - */ - distinct?: Prisma.UploadLogScalarFieldEnum | Prisma.UploadLogScalarFieldEnum[] -} - -/** - * UploadLog findMany - */ -export type UploadLogFindManyArgs = { - /** - * Select specific fields to fetch from the UploadLog - */ - select?: Prisma.UploadLogSelect | null - /** - * Omit specific fields from the UploadLog - */ - omit?: Prisma.UploadLogOmit | null - /** - * Filter, which UploadLogs to fetch. - */ - where?: Prisma.UploadLogWhereInput - /** - * {@link https://www.prisma.io/docs/concepts/components/prisma-client/sorting Sorting Docs} - * - * Determine the order of UploadLogs to fetch. - */ - orderBy?: Prisma.UploadLogOrderByWithRelationInput | Prisma.UploadLogOrderByWithRelationInput[] - /** - * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination#cursor-based-pagination Cursor Docs} - * - * Sets the position for listing UploadLogs. - */ - cursor?: Prisma.UploadLogWhereUniqueInput - /** - * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs} - * - * Take `±n` UploadLogs from the position of the cursor. - */ - take?: number - /** - * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs} - * - * Skip the first `n` UploadLogs. - */ - skip?: number - /** - * {@link https://www.prisma.io/docs/concepts/components/prisma-client/distinct Distinct Docs} - * - * Filter by unique combinations of UploadLogs. - */ - distinct?: Prisma.UploadLogScalarFieldEnum | Prisma.UploadLogScalarFieldEnum[] -} - -/** - * UploadLog create - */ -export type UploadLogCreateArgs = { - /** - * Select specific fields to fetch from the UploadLog - */ - select?: Prisma.UploadLogSelect | null - /** - * Omit specific fields from the UploadLog - */ - omit?: Prisma.UploadLogOmit | null - /** - * The data needed to create a UploadLog. - */ - data: Prisma.XOR -} - -/** - * UploadLog createMany - */ -export type UploadLogCreateManyArgs = { - /** - * The data used to create many UploadLogs. - */ - data: Prisma.UploadLogCreateManyInput | Prisma.UploadLogCreateManyInput[] - skipDuplicates?: boolean -} - -/** - * UploadLog createManyAndReturn - */ -export type UploadLogCreateManyAndReturnArgs = { - /** - * Select specific fields to fetch from the UploadLog - */ - select?: Prisma.UploadLogSelectCreateManyAndReturn | null - /** - * Omit specific fields from the UploadLog - */ - omit?: Prisma.UploadLogOmit | null - /** - * The data used to create many UploadLogs. - */ - data: Prisma.UploadLogCreateManyInput | Prisma.UploadLogCreateManyInput[] - skipDuplicates?: boolean -} - -/** - * UploadLog update - */ -export type UploadLogUpdateArgs = { - /** - * Select specific fields to fetch from the UploadLog - */ - select?: Prisma.UploadLogSelect | null - /** - * Omit specific fields from the UploadLog - */ - omit?: Prisma.UploadLogOmit | null - /** - * The data needed to update a UploadLog. - */ - data: Prisma.XOR - /** - * Choose, which UploadLog to update. - */ - where: Prisma.UploadLogWhereUniqueInput -} - -/** - * UploadLog updateMany - */ -export type UploadLogUpdateManyArgs = { - /** - * The data used to update UploadLogs. - */ - data: Prisma.XOR - /** - * Filter which UploadLogs to update - */ - where?: Prisma.UploadLogWhereInput - /** - * Limit how many UploadLogs to update. - */ - limit?: number -} - -/** - * UploadLog updateManyAndReturn - */ -export type UploadLogUpdateManyAndReturnArgs = { - /** - * Select specific fields to fetch from the UploadLog - */ - select?: Prisma.UploadLogSelectUpdateManyAndReturn | null - /** - * Omit specific fields from the UploadLog - */ - omit?: Prisma.UploadLogOmit | null - /** - * The data used to update UploadLogs. - */ - data: Prisma.XOR - /** - * Filter which UploadLogs to update - */ - where?: Prisma.UploadLogWhereInput - /** - * Limit how many UploadLogs to update. - */ - limit?: number -} - -/** - * UploadLog upsert - */ -export type UploadLogUpsertArgs = { - /** - * Select specific fields to fetch from the UploadLog - */ - select?: Prisma.UploadLogSelect | null - /** - * Omit specific fields from the UploadLog - */ - omit?: Prisma.UploadLogOmit | null - /** - * The filter to search for the UploadLog to update in case it exists. - */ - where: Prisma.UploadLogWhereUniqueInput - /** - * In case the UploadLog found by the `where` argument doesn't exist, create a new UploadLog with this data. - */ - create: Prisma.XOR - /** - * In case the UploadLog was found with the provided `where` argument, update it with this data. - */ - update: Prisma.XOR -} - -/** - * UploadLog delete - */ -export type UploadLogDeleteArgs = { - /** - * Select specific fields to fetch from the UploadLog - */ - select?: Prisma.UploadLogSelect | null - /** - * Omit specific fields from the UploadLog - */ - omit?: Prisma.UploadLogOmit | null - /** - * Filter which UploadLog to delete. - */ - where: Prisma.UploadLogWhereUniqueInput -} - -/** - * UploadLog deleteMany - */ -export type UploadLogDeleteManyArgs = { - /** - * Filter which UploadLogs to delete - */ - where?: Prisma.UploadLogWhereInput - /** - * Limit how many UploadLogs to delete. - */ - limit?: number -} - -/** - * UploadLog without action - */ -export type UploadLogDefaultArgs = { - /** - * Select specific fields to fetch from the UploadLog - */ - select?: Prisma.UploadLogSelect | null - /** - * Omit specific fields from the UploadLog - */ - omit?: Prisma.UploadLogOmit | null -} diff --git a/apps/api/src/prisma/migrations/20260831000000_drop_upload_logs/migration.sql b/apps/api/src/prisma/migrations/20260831000000_drop_upload_logs/migration.sql new file mode 100644 index 0000000..27885ba --- /dev/null +++ b/apps/api/src/prisma/migrations/20260831000000_drop_upload_logs/migration.sql @@ -0,0 +1,6 @@ +-- The upload audit trail was write-only: rows were created on every upload but nothing ever read +-- them, so it only accumulated the Drive owner's file names indefinitely with no retention path +-- and no way to purge them on disconnect. Dropped rather than given a retention policy, since the +-- history view it was built for was never implemented. +DROP TABLE IF EXISTS "upload_logs"; +DROP TYPE IF EXISTS "UploadStatus"; diff --git a/apps/api/src/prisma/schema.prisma b/apps/api/src/prisma/schema.prisma index d24f0e3..faf66a7 100644 --- a/apps/api/src/prisma/schema.prisma +++ b/apps/api/src/prisma/schema.prisma @@ -15,14 +15,6 @@ datasource db { provider = "postgresql" } -// -// Enums -// -enum UploadStatus { - SUCCESS - FAILED -} - // // Models // @@ -88,18 +80,3 @@ model RefreshToken { @@index([expiresAt], map: "idx_refresh_token_expires_at") @@map("refresh_tokens") } - -/// Audit trail of upload attempts for the history view. -model UploadLog { - id String @id @default(uuid()) - fileName String @map("file_name") - folderId String @map("folder_id") - fileId String? @map("file_id") - size BigInt? - status UploadStatus - error String? - createdAt DateTime @default(now()) @map("created_at") - - @@index([createdAt], map: "idx_upload_log_created_at") - @@map("upload_logs") -} diff --git a/apps/api/src/prisma/seed.ts b/apps/api/src/prisma/seed.ts deleted file mode 100644 index 4d4dd44..0000000 --- a/apps/api/src/prisma/seed.ts +++ /dev/null @@ -1,29 +0,0 @@ -import 'dotenv/config'; -import { Logger } from '@nestjs/common'; -import { PrismaPg } from '@prisma/adapter-pg'; - -import { databaseConfig } from '@/config/database.config'; - -import { PrismaClient } from './generated/prisma/client'; - -const logger = new Logger('Seed'); - -const adapter = new PrismaPg({ - connectionString: databaseConfig().url, -}); - -const prisma = new PrismaClient({ adapter }); - -async function main() { - // No seed data required — accounts/folders are created at runtime via the OAuth + Picker flow. -} - -main() - .then(async () => await prisma.$disconnect()) - .catch(async (error) => { - logger.error(`Error during seeding: ${error}`); - - await prisma.$disconnect(); - - process.exit(1); - }); diff --git a/apps/api/test/auth/auth.e2e-spec.ts b/apps/api/test/auth/auth.e2e-spec.ts index 8326ec0..1d76f06 100644 --- a/apps/api/test/auth/auth.e2e-spec.ts +++ b/apps/api/test/auth/auth.e2e-spec.ts @@ -113,6 +113,44 @@ describe('Google Auth (integration)', () => { }); }); + describe('DELETE /api/v1/google-auth/folders/:folderId', () => { + it('returns 403 without a verified owner cookie', async () => { + prisma.driveAccount.findFirst.mockResolvedValue({ id: 'acc-1', email: 'owner@gmail.com' }); + + const res = await request(app.getHttpServer()) + .delete('/api/v1/google-auth/folders/drive-1') + .set('Cookie', accessCookie()); + + // Being a logged-in operator is not enough — unauthorizing a folder is the owner's call. + expect(res.status).toBe(403); + expect(prisma.allowedFolder.deleteMany).not.toHaveBeenCalled(); + }); + + it('returns 403 for an owner cookie naming a different account', async () => { + prisma.driveAccount.findFirst.mockResolvedValue({ id: 'acc-1', email: 'current@gmail.com' }); + + const res = await request(app.getHttpServer()) + .delete('/api/v1/google-auth/folders/drive-1') + .set('Cookie', `${accessCookie()}; ${ownerCookie('previous@gmail.com')}`); + + expect(res.status).toBe(403); + expect(prisma.allowedFolder.deleteMany).not.toHaveBeenCalled(); + }); + + it('removes the folder for the verified owner (204)', async () => { + prisma.driveAccount.findFirst.mockResolvedValue({ id: 'acc-1', email: 'owner@gmail.com' }); + + const res = await request(app.getHttpServer()) + .delete('/api/v1/google-auth/folders/drive-1') + .set('Cookie', `${accessCookie()}; ${ownerCookie('owner@gmail.com')}`); + + expect(res.status).toBe(204); + expect(prisma.allowedFolder.deleteMany).toHaveBeenCalledWith({ + where: { driveAccountId: 'acc-1', folderId: 'drive-1' }, + }); + }); + }); + describe('GET /api/v1/google-auth/picker-token', () => { it('returns a short-lived access token for the Picker', async () => { prisma.driveAccount.findFirst.mockResolvedValue({ id: 'acc-1' }); diff --git a/apps/api/test/drive/drive.e2e-spec.ts b/apps/api/test/drive/drive.e2e-spec.ts index 6abd451..21a0bde 100644 --- a/apps/api/test/drive/drive.e2e-spec.ts +++ b/apps/api/test/drive/drive.e2e-spec.ts @@ -1,4 +1,5 @@ import { INestApplication } from '@nestjs/common'; +import { Readable } from 'node:stream'; import request from 'supertest'; import { accessCookie } from '../helpers/auth.helper'; @@ -11,6 +12,9 @@ jest.mock('googleapis', () => require('../helpers/googleapis.mock').createGoogle const FOLDER_MIME = 'application/vnd.google-apps.folder'; +// From the environment: a drifted local .env.test must not turn into a false failure. +const MAX_UPLOAD_BYTES = Number(process.env['MAX_UPLOAD_BYTES']); + describe('Drive (integration)', () => { let app: INestApplication; const prisma: PrismaMock = createPrismaMock(); @@ -68,7 +72,9 @@ describe('Drive (integration)', () => { prisma.allowedFolder.findMany.mockResolvedValue([ { id: 'f1', folderId: 'drive-1', name: 'Photos', createdAt: new Date() }, ]); - driveFilesMock.get.mockRejectedValue(Object.assign(new Error('File not found'), { code: 404 })); + driveFilesMock.get.mockRejectedValue( + Object.assign(new Error('File not found'), { code: 404 }), + ); const res = await request(app.getHttpServer()) .get('/api/v1/storage/drive/folders') @@ -107,10 +113,20 @@ describe('Drive (integration)', () => { it('resolves the display name + Drive link for each requested id', async () => { connectAccountWithRoots('root-1'); - driveFilesMock.get.mockImplementation(({ fileId }: { fileId: string }) => - Promise.resolve({ - data: { id: fileId, name: `folder-${fileId}`, webViewLink: `http://view/${fileId}` }, - }), + // "a" and "b" sit inside the authorized root; the walk runs before the metadata read. + driveFilesMock.get.mockImplementation( + ({ fileId, fields }: { fileId: string; fields: string }) => + Promise.resolve( + fields === 'id, parents, trashed' + ? { data: { id: fileId, parents: ['root-1'], trashed: false } } + : { + data: { + id: fileId, + name: `folder-${fileId}`, + webViewLink: `http://view/${fileId}`, + }, + }, + ), ); const res = await request(app.getHttpServer()) @@ -124,6 +140,323 @@ describe('Drive (integration)', () => { { id: 'b', name: 'folder-b', webViewLink: 'http://view/b' }, ]); }); + + it('returns an empty placeholder for an id outside the authorized tree', async () => { + connectAccountWithRoots('root-1'); + // Parented somewhere else in the owner's Drive: reachable by the OAuth grant, not by the app. + driveFilesMock.get.mockImplementation( + ({ fileId, fields }: { fileId: string; fields: string }) => + Promise.resolve( + fields === 'id, parents, trashed' + ? { data: { id: fileId, parents: ['someone-elses-folder'], trashed: false } } + : { data: { id: fileId, name: 'Tax return.pdf', webViewLink: 'http://view/secret' } }, + ), + ); + + const res = await request(app.getHttpServer()) + .get('/api/v1/storage/drive/names') + .query({ ids: 'outsider' }) + .set('Cookie', accessCookie()); + + expect(res.status).toBe(200); + expect(res.body).toEqual([{ id: 'outsider', name: '', webViewLink: null }]); + }); + + it('rejects more ids than the per-request cap (400)', async () => { + connectAccountWithRoots('root-1'); + + const res = await request(app.getHttpServer()) + .get('/api/v1/storage/drive/names') + .query({ ids: Array.from({ length: 51 }, (_, i) => `id-${i}`).join(',') }) + .set('Cookie', accessCookie()); + + expect(res.status).toBe(400); + expect(driveFilesMock.get).not.toHaveBeenCalled(); + }); + }); + + describe('GET /api/v1/storage/drive/folders/:id/contents', () => { + it('lists a folder inside the authorized tree', async () => { + connectAccountWithRoots('root-1'); + driveFilesMock.list.mockResolvedValue({ + data: { + files: [ + { id: 'sub', name: 'Sub', mimeType: FOLDER_MIME }, + { id: 'doc', name: 'doc.pdf', mimeType: 'application/pdf', size: '2048' }, + ], + }, + }); + + const res = await request(app.getHttpServer()) + .get('/api/v1/storage/drive/folders/root-1/contents') + .set('Cookie', accessCookie()); + + expect(res.status).toBe(200); + expect(res.body.entries).toHaveLength(2); + expect(res.body.entries[0]).toMatchObject({ id: 'sub', isFolder: true }); + expect(res.body.nextPageToken).toBeNull(); + }); + + it('refuses a folder outside the authorized tree (403)', async () => { + connectAccountWithRoots('root-1'); + // Reachable by the OAuth grant (it covers the whole Drive) but not parented under a root. + driveFilesMock.get.mockResolvedValue({ data: { id: 'outsider', parents: [] } }); + + const res = await request(app.getHttpServer()) + .get('/api/v1/storage/drive/folders/outsider/contents') + .set('Cookie', accessCookie()); + + expect(res.status).toBe(403); + expect(driveFilesMock.list).not.toHaveBeenCalled(); + }); + + it('normalizes unknown sort parameters instead of passing them through', async () => { + connectAccountWithRoots('root-1'); + driveFilesMock.list.mockResolvedValue({ data: { files: [] } }); + + const res = await request(app.getHttpServer()) + .get('/api/v1/storage/drive/folders/root-1/contents') + .query({ sort: 'sqli', dir: 'sideways' }) + .set('Cookie', accessCookie()); + + expect(res.status).toBe(200); + // Falls back to the name/asc default; the raw value never reaches the Drive query. + expect(driveFilesMock.list).toHaveBeenCalledWith( + expect.objectContaining({ orderBy: expect.stringContaining('name') }), + ); + expect(JSON.stringify(driveFilesMock.list.mock.calls)).not.toContain('sqli'); + }); + + it('rejects a request without an auth cookie (401)', async () => { + const res = await request(app.getHttpServer()).get( + '/api/v1/storage/drive/folders/root-1/contents', + ); + + expect(res.status).toBe(401); + }); + }); + + describe('PATCH /api/v1/storage/drive/files/:id/rename', () => { + it('renames an item inside the authorized tree', async () => { + connectAccountWithRoots('root-1'); + driveFilesMock.get.mockResolvedValue({ data: { id: 'doc', parents: ['root-1'] } }); + driveFilesMock.update.mockResolvedValue({ + data: { id: 'doc', name: 'renamed.pdf', mimeType: 'application/pdf' }, + }); + + const res = await request(app.getHttpServer()) + .patch('/api/v1/storage/drive/files/doc/rename') + .set('Cookie', accessCookie()) + .send({ name: 'renamed.pdf' }); + + expect(res.status).toBe(200); + expect(res.body).toMatchObject({ id: 'doc', name: 'renamed.pdf' }); + }); + + it('rejects a name containing a path separator (400)', async () => { + connectAccountWithRoots('root-1'); + + const res = await request(app.getHttpServer()) + .patch('/api/v1/storage/drive/files/doc/rename') + .set('Cookie', accessCookie()) + .send({ name: '../escaped.pdf' }); + + expect(res.status).toBe(400); + expect(driveFilesMock.update).not.toHaveBeenCalled(); + }); + + it('rejects an empty name (400)', async () => { + connectAccountWithRoots('root-1'); + + const res = await request(app.getHttpServer()) + .patch('/api/v1/storage/drive/files/doc/rename') + .set('Cookie', accessCookie()) + .send({ name: '' }); + + expect(res.status).toBe(400); + }); + + it('strips unknown properties rather than forwarding them (400)', async () => { + connectAccountWithRoots('root-1'); + + const res = await request(app.getHttpServer()) + .patch('/api/v1/storage/drive/files/doc/rename') + .set('Cookie', accessCookie()) + .send({ name: 'ok.pdf', fileId: 'somebody-elses-file' }); + + // forbidNonWhitelisted: an unexpected field is a 400, not a silently ignored one. + expect(res.status).toBe(400); + }); + + it('refuses to rename an authorized root (409)', async () => { + connectAccountWithRoots('root-1'); + + const res = await request(app.getHttpServer()) + .patch('/api/v1/storage/drive/files/root-1/rename') + .set('Cookie', accessCookie()) + .send({ name: 'Renamed root' }); + + expect(res.status).toBe(409); + expect(driveFilesMock.update).not.toHaveBeenCalled(); + }); + }); + + describe('PATCH /api/v1/storage/drive/files/:id/move', () => { + it('moves an item between authorized folders', async () => { + connectAccountWithRoots('root-1'); + driveFilesMock.get.mockImplementation(({ fileId }: { fileId: string }) => + Promise.resolve({ data: { id: fileId, parents: ['root-1'] } }), + ); + driveFilesMock.update.mockResolvedValue({ + data: { id: 'doc', name: 'doc.pdf', mimeType: 'application/pdf' }, + }); + + const res = await request(app.getHttpServer()) + .patch('/api/v1/storage/drive/files/doc/move') + .set('Cookie', accessCookie()) + .send({ targetFolderId: 'sub-1' }); + + expect(res.status).toBe(200); + expect(driveFilesMock.update).toHaveBeenCalledWith( + expect.objectContaining({ addParents: 'sub-1', removeParents: 'root-1' }), + ); + }); + + it('rejects moving an item into itself (400)', async () => { + connectAccountWithRoots('root-1'); + + const res = await request(app.getHttpServer()) + .patch('/api/v1/storage/drive/files/doc/move') + .set('Cookie', accessCookie()) + .send({ targetFolderId: 'doc' }); + + expect(res.status).toBe(400); + }); + + it('refuses a destination outside the authorized tree (403)', async () => { + connectAccountWithRoots('root-1'); + driveFilesMock.get.mockImplementation(({ fileId }: { fileId: string }) => + Promise.resolve({ data: { id: fileId, parents: fileId === 'doc' ? ['root-1'] : [] } }), + ); + + const res = await request(app.getHttpServer()) + .patch('/api/v1/storage/drive/files/doc/move') + .set('Cookie', accessCookie()) + .send({ targetFolderId: 'elsewhere' }); + + expect(res.status).toBe(403); + expect(driveFilesMock.update).not.toHaveBeenCalled(); + }); + }); + + describe('GET /api/v1/storage/drive/files/:id/download', () => { + /** + * Scripts an authorized file whose media request yields the given body. + **/ + const withDownloadableFile = (name: string, body: string) => { + connectAccountWithRoots('root-1'); + driveFilesMock.get.mockImplementation( + ({ alt, fields }: { alt?: string; fields?: string }) => { + if (alt === 'media') return Promise.resolve({ data: Readable.from([body]) }); + if (fields === 'name, mimeType, size') { + return Promise.resolve({ + data: { name, mimeType: 'text/plain', size: String(body.length) }, + }); + } + return Promise.resolve({ data: { id: 'doc', parents: ['root-1'] } }); + }, + ); + }; + + it('streams the file with its length and disposition', async () => { + withDownloadableFile('note.txt', 'hello'); + + const res = await request(app.getHttpServer()) + .get('/api/v1/storage/drive/files/doc/download') + .set('Cookie', accessCookie()); + + expect(res.status).toBe(200); + expect(res.headers['content-type']).toContain('text/plain'); + expect(res.headers['content-length']).toBe('5'); + expect(res.headers['content-disposition']).toBe( + 'attachment; filename="note.txt"; filename*=UTF-8\'\'note.txt', + ); + }); + + it('escapes a non-ASCII filename into an ASCII fallback plus an RFC 5987 variant', async () => { + withDownloadableFile('účtenka "2026".txt', 'x'); + + const res = await request(app.getHttpServer()) + .get('/api/v1/storage/drive/files/doc/download') + .set('Cookie', accessCookie()); + + const disposition = res.headers['content-disposition'] as string; + // The quoted fallback must stay ASCII and must not contain a quote that ends it early. + expect(disposition).toContain('filename="__tenka 2026.txt"'); + expect(disposition).toContain("filename*=UTF-8''%C3%BA%C4%8Dtenka"); + }); + + it('refuses a file outside the authorized tree (403)', async () => { + connectAccountWithRoots('root-1'); + driveFilesMock.get.mockResolvedValue({ data: { id: 'outsider', parents: [] } }); + + const res = await request(app.getHttpServer()) + .get('/api/v1/storage/drive/files/outsider/download') + .set('Cookie', accessCookie()); + + expect(res.status).toBe(403); + }); + }); + + describe('GET /api/v1/storage/drive/folders/:id/download', () => { + it('streams an authorized subfolder as a ZIP with a filename', async () => { + connectAccountWithRoots('root-1'); + driveFilesMock.get.mockImplementation(({ fields }: { fields?: string }) => + Promise.resolve( + fields === 'name, mimeType' + ? { data: { name: 'Sub', mimeType: FOLDER_MIME } } + : { data: { id: 'sub', parents: ['root-1'] } }, + ), + ); + driveFilesMock.list.mockResolvedValue({ data: { files: [] } }); + + const res = await request(app.getHttpServer()) + .get('/api/v1/storage/drive/folders/sub/download') + .set('Cookie', accessCookie()) + .buffer() + .parse((response, callback) => { + const chunks: Buffer[] = []; + response.on('data', (chunk: Buffer) => chunks.push(chunk)); + response.on('end', () => callback(null, Buffer.concat(chunks))); + }); + + expect(res.status).toBe(200); + expect(res.headers['content-type']).toBe('application/zip'); + expect(res.headers['content-disposition']).toContain('filename="Sub.zip"'); + // "PK" — a real archive came back, not an empty body with ZIP headers bolted on. + expect((res.body as Buffer).subarray(0, 2).toString()).toBe('PK'); + }); + + it('refuses to ZIP an authorized root (400)', async () => { + connectAccountWithRoots('root-1'); + + const res = await request(app.getHttpServer()) + .get('/api/v1/storage/drive/folders/root-1/download') + .set('Cookie', accessCookie()); + + expect(res.status).toBe(400); + }); + + it('refuses a folder outside the authorized tree (403)', async () => { + connectAccountWithRoots('root-1'); + driveFilesMock.get.mockResolvedValue({ data: { id: 'outsider', parents: [] } }); + + const res = await request(app.getHttpServer()) + .get('/api/v1/storage/drive/folders/outsider/download') + .set('Cookie', accessCookie()); + + expect(res.status).toBe(403); + }); }); describe('POST /api/v1/storage/drive/folders/:id/subfolder', () => { @@ -183,9 +516,6 @@ describe('Drive (integration)', () => { size: 5, webViewLink: 'http://view/up-1', }); - expect(prisma.uploadLog.create).toHaveBeenCalledWith( - expect.objectContaining({ data: expect.objectContaining({ status: 'SUCCESS' }) }), - ); }); it('returns 400 when no file is attached', async () => { @@ -197,6 +527,23 @@ describe('Drive (integration)', () => { expect(res.status).toBe(400); }); + + it('rejects a file over MAX_UPLOAD_BYTES (400)', async () => { + connectAccountWithRoots('root-1'); + // Drain like a real upload (backpressure would stall it) but never settle: the 400 must come from busboy's limit. + driveFilesMock.create.mockImplementation(({ media }: { media?: { body?: Readable } }) => { + media?.body?.resume(); + + return new Promise(() => {}); + }); + + const res = await request(app.getHttpServer()) + .post('/api/v1/storage/drive/folders/root-1/upload') + .set('Cookie', accessCookie()) + .attach('file', Buffer.alloc(MAX_UPLOAD_BYTES + 1024, 'x'), 'huge.bin'); + + expect(res.status).toBe(400); + }); }); describe('DELETE /api/v1/storage/drive/files/:id', () => { diff --git a/apps/api/test/google-auth/google-auth.e2e-spec.ts b/apps/api/test/google-auth/google-auth.e2e-spec.ts index cc6ae40..65a9cae 100644 --- a/apps/api/test/google-auth/google-auth.e2e-spec.ts +++ b/apps/api/test/google-auth/google-auth.e2e-spec.ts @@ -8,7 +8,9 @@ import { createTestApp } from '../helpers/test-app.helper'; jest.mock('googleapis', () => require('../helpers/googleapis.mock').createGoogleApisMock()); -/** Guards the OAuth consent flow: operator-only initiation and cookie-bound `state` nonce. */ +/** + * Guards the OAuth consent flow: operator-only initiation and cookie-bound `state` nonce. + **/ describe('Google Auth OAuth flow (integration)', () => { let app: INestApplication; const prisma: PrismaMock = createPrismaMock(); diff --git a/apps/api/test/helpers/fixtures.helper.ts b/apps/api/test/helpers/fixtures.helper.ts index 6dfc10a..e5efe89 100644 --- a/apps/api/test/helpers/fixtures.helper.ts +++ b/apps/api/test/helpers/fixtures.helper.ts @@ -1,6 +1,6 @@ import { CryptoService } from '@/common/services/crypto/crypto.service'; -// Mirrors TOKEN_ENCRYPTION_KEY from setup-env.ts (all-zero 32-byte key). +// Mirrors TOKEN_ENCRYPTION_KEY from .env.test (all-zero 32-byte key; see .env.test.example). const TEST_ENCRYPTION_KEY = Buffer.from('0'.repeat(64), 'hex'); /** A refresh token encrypted with the test key so the real CryptoService can decrypt it. */ diff --git a/apps/api/test/helpers/googleapis.mock.ts b/apps/api/test/helpers/googleapis.mock.ts index b53ee61..16ed7c2 100644 --- a/apps/api/test/helpers/googleapis.mock.ts +++ b/apps/api/test/helpers/googleapis.mock.ts @@ -13,6 +13,7 @@ export const driveFilesMock = { list: jest.fn(), create: jest.fn(), delete: jest.fn(), + update: jest.fn(), }; /** @@ -38,4 +39,5 @@ export const resetGoogleApisMock = (): void => { driveFilesMock.list.mockReset(); driveFilesMock.create.mockReset(); driveFilesMock.delete.mockReset(); + driveFilesMock.update.mockReset(); }; diff --git a/apps/api/test/helpers/prisma.mock.ts b/apps/api/test/helpers/prisma.mock.ts index e00aae3..662fae1 100644 --- a/apps/api/test/helpers/prisma.mock.ts +++ b/apps/api/test/helpers/prisma.mock.ts @@ -25,7 +25,6 @@ export interface PrismaMock { updateMany: jest.Mock; deleteMany: jest.Mock; }; - uploadLog: { create: jest.Mock }; } /** @@ -76,7 +75,6 @@ export const createPrismaMock = (): PrismaMock => { updateMany: jest.fn().mockResolvedValue({ count: 1 }), deleteMany: jest.fn().mockResolvedValue({ count: 0 }), }, - uploadLog: { create: jest.fn().mockResolvedValue(undefined) }, }; return mock; @@ -107,5 +105,4 @@ export const resetPrismaMock = (mock: PrismaMock): void => { mock.refreshToken.update.mockReset().mockResolvedValue(undefined); mock.refreshToken.updateMany.mockReset().mockResolvedValue({ count: 1 }); mock.refreshToken.deleteMany.mockReset().mockResolvedValue({ count: 0 }); - mock.uploadLog.create.mockReset().mockResolvedValue(undefined); }; diff --git a/apps/api/test/helpers/test-app.helper.ts b/apps/api/test/helpers/test-app.helper.ts index 989b94f..0eb05d3 100644 --- a/apps/api/test/helpers/test-app.helper.ts +++ b/apps/api/test/helpers/test-app.helper.ts @@ -1,9 +1,9 @@ -import { INestApplication, RequestMethod, ValidationPipe } from '@nestjs/common'; +import { INestApplication } from '@nestjs/common'; import { NestExpressApplication } from '@nestjs/platform-express'; import { Test } from '@nestjs/testing'; -import cookieParser from 'cookie-parser'; import { AppModule } from '@/app.module'; +import { configureApp } from '@/app.setup'; import { PrismaService } from '@/prisma/prisma.service'; import { PrismaMock } from './prisma.mock'; @@ -11,6 +11,7 @@ import { PrismaMock } from './prisma.mock'; /** * Boots the full AppModule for integration tests with PrismaService mocked. **/ +// The same `configureApp` call `main.ts` makes, so the suite exercises the real pipeline, not a copied subset. export async function createTestApp(prisma: PrismaMock): Promise { const moduleRef = await Test.createTestingModule({ imports: [AppModule] }) .overrideProvider(PrismaService) @@ -20,14 +21,7 @@ export async function createTestApp(prisma: PrismaMock): Promise({ logger: false }); - app.set('trust proxy', 1); - app.setGlobalPrefix('/api/v1', { - exclude: [{ path: 'health', method: RequestMethod.GET }], - }); - app.use(cookieParser()); - app.useGlobalPipes( - new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true, transform: true }), - ); + configureApp(app); await app.init(); diff --git a/apps/api/test/infra.e2e-spec.ts b/apps/api/test/infra.e2e-spec.ts new file mode 100644 index 0000000..d3e8d36 --- /dev/null +++ b/apps/api/test/infra.e2e-spec.ts @@ -0,0 +1,112 @@ +import { INestApplication } from '@nestjs/common'; +import request from 'supertest'; + +import { createPrismaMock, PrismaMock, resetPrismaMock } from './helpers/prisma.mock'; +import { createTestApp } from './helpers/test-app.helper'; + +jest.mock('googleapis', () => require('./helpers/googleapis.mock').createGoogleApisMock()); + +// CORS_ALLOWED_ORIGINS in .env.test. +const ALLOWED_ORIGIN = 'http://localhost:3000'; + +/** + * The cross-cutting HTTP setup itself: prefix, security headers and the CORS allowlist. + **/ +// From `configureApp`; the harness used to re-declare these and omit helmet/CORS, so nothing covered them. +describe('App setup (integration)', () => { + let app: INestApplication; + const prisma: PrismaMock = createPrismaMock(); + + beforeAll(async () => { + app = await createTestApp(prisma); + }); + + afterAll(async () => { + await app.close(); + }); + + beforeEach(() => { + resetPrismaMock(prisma); + }); + + describe('global prefix', () => { + it('serves the API under /api/v1', async () => { + const res = await request(app.getHttpServer()).get('/api/v1/auth/me'); + + // 401, not 404: the route exists and the guard answered. + expect(res.status).toBe(401); + }); + + it('exempts the health probe from the prefix', async () => { + const res = await request(app.getHttpServer()).get('/health'); + + expect(res.status).not.toBe(404); + }); + + it('404s a route outside the prefix', async () => { + const res = await request(app.getHttpServer()).get('/auth/me'); + + expect(res.status).toBe(404); + }); + }); + + describe('security headers (helmet)', () => { + it('sets nosniff and denies framing', async () => { + const res = await request(app.getHttpServer()).get('/api/v1/auth/me'); + + expect(res.headers['x-content-type-options']).toBe('nosniff'); + expect(res.headers['x-frame-options']).toBe('SAMEORIGIN'); + }); + + it('does not advertise the framework', async () => { + const res = await request(app.getHttpServer()).get('/api/v1/auth/me'); + + expect(res.headers['x-powered-by']).toBeUndefined(); + }); + + it('leaves CSP unset (JSON API, and a default policy breaks dev Swagger UI)', async () => { + const res = await request(app.getHttpServer()).get('/api/v1/auth/me'); + + expect(res.headers['content-security-policy']).toBeUndefined(); + }); + }); + + describe('CORS allowlist', () => { + it('allows the configured web app origin with credentials', async () => { + const res = await request(app.getHttpServer()) + .get('/api/v1/auth/me') + .set('Origin', ALLOWED_ORIGIN); + + expect(res.headers['access-control-allow-origin']).toBe(ALLOWED_ORIGIN); + // Auth rides in cookies, so the browser only sends them when this is true. + expect(res.headers['access-control-allow-credentials']).toBe('true'); + }); + + it('refuses to echo an origin that is not on the allowlist', async () => { + const res = await request(app.getHttpServer()) + .get('/api/v1/auth/me') + .set('Origin', 'https://evil.example.com'); + + // No header at all — the browser then blocks the response from reaching the page. + expect(res.headers['access-control-allow-origin']).toBeUndefined(); + }); + + it('never answers with a wildcard, which credentials would make unusable anyway', async () => { + const res = await request(app.getHttpServer()) + .get('/api/v1/auth/me') + .set('Origin', ALLOWED_ORIGIN); + + expect(res.headers['access-control-allow-origin']).not.toBe('*'); + }); + + it('answers a preflight for an allowed origin', async () => { + const res = await request(app.getHttpServer()) + .options('/api/v1/auth/login') + .set('Origin', ALLOWED_ORIGIN) + .set('Access-Control-Request-Method', 'POST'); + + expect(res.status).toBeLessThan(300); + expect(res.headers['access-control-allow-methods']).toContain('POST'); + }); + }); +}); diff --git a/apps/api/test/jest-e2e.json b/apps/api/test/jest-e2e.json index a860874..ae12f2b 100644 --- a/apps/api/test/jest-e2e.json +++ b/apps/api/test/jest-e2e.json @@ -1,14 +1,25 @@ { - "moduleFileExtensions": ["js", "json", "ts"], + "moduleFileExtensions": [ + "js", + "json", + "ts" + ], "rootDir": "..", - "roots": ["/test"], + "roots": [ + "/test" + ], "testRegex": ".e2e-spec\\.ts$", "transformIgnorePatterns": [ - "/node_modules/.pnpm/(?!(cookie|archiver|compress-commons|crc32-stream|is-stream|zip-stream)@)", + "/node_modules/", "\\.pnp\\.[^/]+$" ], "transform": { - "^.+\\.(t|j)s$": ["ts-jest", { "tsconfig": "/tsconfig.spec.json" }] + "^.+\\.(t|j)s$": [ + "ts-jest", + { + "tsconfig": "/tsconfig.spec.json" + } + ] }, "moduleNameMapper": { "^(\\.{1,2}/.*)\\.js$": "$1", @@ -17,7 +28,9 @@ "^src/(.*)$": "/src/$1" }, "testEnvironment": "node", - "setupFiles": ["/test/setup-env.ts"], + "setupFiles": [ + "/test/setup-env.ts" + ], "testTimeout": 30000, "maxWorkers": 1, "forceExit": true diff --git a/apps/api/test/setup-env.ts b/apps/api/test/setup-env.ts index a0051fe..d017cf0 100644 --- a/apps/api/test/setup-env.ts +++ b/apps/api/test/setup-env.ts @@ -7,3 +7,13 @@ config({ path: resolve(__dirname, '../.env.test'), override: true }); // Silence Nest's logger so test output stays readable (services log on success/error paths). Logger.overrideLogger(false); + +// The Drive provider reaches Google over raw `fetch`, which the googleapis mock does not cover; specs stub it themselves. +global.fetch = ((input: RequestInfo | URL): never => { + const url = + typeof input === 'string' ? input : input instanceof URL ? input.href : (input as Request).url; + + throw new Error( + `Unmocked outbound fetch to ${url}. Stub global.fetch in the test rather than calling a real service.`, + ); +}) as unknown as typeof fetch; diff --git a/apps/api/test/storage/resumable-upload.e2e-spec.ts b/apps/api/test/storage/resumable-upload.e2e-spec.ts new file mode 100644 index 0000000..65e2ec2 --- /dev/null +++ b/apps/api/test/storage/resumable-upload.e2e-spec.ts @@ -0,0 +1,239 @@ +import { INestApplication } from '@nestjs/common'; +import request from 'supertest'; + +import { accessCookie } from '../helpers/auth.helper'; +import { encryptedTestRefreshToken } from '../helpers/fixtures.helper'; +import { driveFilesMock, resetGoogleApisMock } from '../helpers/googleapis.mock'; +import { createPrismaMock, PrismaMock, resetPrismaMock } from '../helpers/prisma.mock'; +import { createTestApp } from '../helpers/test-app.helper'; + +jest.mock('googleapis', () => require('../helpers/googleapis.mock').createGoogleApisMock()); + +// From the environment: a drifted local .env.test must not turn into a false failure. +const MAX_UPLOAD_BYTES = Number(process.env['MAX_UPLOAD_BYTES']); +const OFFLINE_TIMEOUT_MS = Number(process.env['OFFLINE_TIMEOUT_MS']); + +/** + * The resumable flow: the API opens the session, reports progress and validates the finished file. + **/ +// These three reach Google over raw `fetch`; test/setup-env.ts turns an unstubbed call into a loud failure. +describe('Resumable upload (integration)', () => { + let app: INestApplication; + let fetchMock: jest.Mock; + const prisma: PrismaMock = createPrismaMock(); + + /** + * Scripts a connected account with the given authorized root folder ids. + **/ + const connectAccountWithRoots = (...folderIds: string[]) => { + prisma.driveAccount.findFirst.mockResolvedValue({ id: 'acc-1' }); + prisma.driveAccount.findUnique.mockResolvedValue({ + id: 'acc-1', + refreshTokenEnc: encryptedTestRefreshToken, + }); + prisma.allowedFolder.findMany.mockResolvedValue(folderIds.map((folderId) => ({ folderId }))); + }; + + beforeAll(async () => { + app = await createTestApp(prisma); + }); + + afterAll(async () => { + await app.close(); + }); + + beforeEach(() => { + resetPrismaMock(prisma); + resetGoogleApisMock(); + fetchMock = jest.fn(); + global.fetch = fetchMock as unknown as typeof fetch; + }); + + describe('POST /api/v1/storage/drive/folders/:id/upload-session', () => { + it('opens a session and hands back the URL plus the server-set offline timeout', async () => { + connectAccountWithRoots('root-1'); + fetchMock.mockResolvedValue({ + ok: true, + status: 200, + headers: new Headers({ + location: 'https://www.googleapis.com/upload/drive/v3/files?upload_id=X', + }), + }); + + const res = await request(app.getHttpServer()) + .post('/api/v1/storage/drive/folders/root-1/upload-session') + .set('Cookie', accessCookie()) + .send({ name: 'big.bin', mimeType: 'application/octet-stream', size: 1024 }); + + expect(res.status).toBe(201); + expect(res.body).toEqual({ + uploadUrl: 'https://www.googleapis.com/upload/drive/v3/files?upload_id=X', + offlineTimeoutMs: OFFLINE_TIMEOUT_MS, + }); + }); + + it('refuses a file over the configured maximum (400)', async () => { + connectAccountWithRoots('root-1'); + + const res = await request(app.getHttpServer()) + .post('/api/v1/storage/drive/folders/root-1/upload-session') + .set('Cookie', accessCookie()) + .send({ + name: 'huge.bin', + mimeType: 'application/octet-stream', + size: MAX_UPLOAD_BYTES + 1, + }); + + expect(res.status).toBe(400); + expect(fetchMock).not.toHaveBeenCalled(); + }); + + it('refuses a folder outside the authorized tree (403)', async () => { + connectAccountWithRoots('root-1'); + driveFilesMock.get.mockResolvedValue({ data: { id: 'outsider', parents: [] } }); + + const res = await request(app.getHttpServer()) + .post('/api/v1/storage/drive/folders/outsider/upload-session') + .set('Cookie', accessCookie()) + .send({ name: 'big.bin', mimeType: 'application/octet-stream', size: 1024 }); + + expect(res.status).toBe(403); + expect(fetchMock).not.toHaveBeenCalled(); + }); + + it.each([ + ['a missing name', { mimeType: 'text/plain', size: 10 }], + ['a zero size', { name: 'a.txt', mimeType: 'text/plain', size: 0 }], + ['a non-integer size', { name: 'a.txt', mimeType: 'text/plain', size: 1.5 }], + ['an unknown field', { name: 'a.txt', mimeType: 'text/plain', size: 10, parents: ['x'] }], + ])('rejects %s (400)', async (_label, body) => { + connectAccountWithRoots('root-1'); + + const res = await request(app.getHttpServer()) + .post('/api/v1/storage/drive/folders/root-1/upload-session') + .set('Cookie', accessCookie()) + .send(body); + + expect(res.status).toBe(400); + }); + + it('rejects a request without an auth cookie (401)', async () => { + const res = await request(app.getHttpServer()) + .post('/api/v1/storage/drive/folders/root-1/upload-session') + .send({ name: 'big.bin', mimeType: 'application/octet-stream', size: 1024 }); + + expect(res.status).toBe(401); + }); + }); + + describe('POST /api/v1/storage/drive/upload-status', () => { + it('reports how many bytes a session has received', async () => { + connectAccountWithRoots('root-1'); + fetchMock.mockResolvedValue({ status: 308, headers: new Headers({ range: 'bytes=0-99' }) }); + + const res = await request(app.getHttpServer()) + .post('/api/v1/storage/drive/upload-status') + .set('Cookie', accessCookie()) + .send({ + uploadUrl: 'https://www.googleapis.com/upload/drive/v3/files?upload_id=X', + size: 500, + }); + + expect(res.status).toBe(201); + expect(res.body).toEqual({ complete: false, receivedBytes: 100, fileId: null }); + }); + + it('reports a finished session with the resulting file id', async () => { + connectAccountWithRoots('root-1'); + fetchMock.mockResolvedValue({ + status: 200, + headers: new Headers(), + json: jest.fn().mockResolvedValue({ id: 'file-1' }), + }); + + const res = await request(app.getHttpServer()) + .post('/api/v1/storage/drive/upload-status') + .set('Cookie', accessCookie()) + .send({ + uploadUrl: 'https://www.googleapis.com/upload/drive/v3/files?upload_id=X', + size: 500, + }); + + expect(res.status).toBe(201); + expect(res.body).toEqual({ complete: true, receivedBytes: 500, fileId: 'file-1' }); + }); + + // The URL is request-supplied, so a relaxed host check would make this an SSRF primitive. + it.each([ + ['an unrelated host', 'https://evil.example.com/upload/drive/v3/files'], + ['plain http', 'http://www.googleapis.com/upload/drive/v3/files'], + ['a cloud metadata address', 'http://169.254.169.254/latest/meta-data/'], + ['a look-alike host', 'https://www.googleapis.com.evil.test/upload/drive/'], + ['a loopback address', 'http://127.0.0.1:6379/'], + ])('refuses to dial %s (400)', async (_label, uploadUrl) => { + connectAccountWithRoots('root-1'); + + const res = await request(app.getHttpServer()) + .post('/api/v1/storage/drive/upload-status') + .set('Cookie', accessCookie()) + .send({ uploadUrl, size: 500 }); + + expect(res.status).toBe(400); + expect(fetchMock).not.toHaveBeenCalled(); + }); + + it('rejects a request without an auth cookie (401)', async () => { + const res = await request(app.getHttpServer()) + .post('/api/v1/storage/drive/upload-status') + .send({ uploadUrl: 'https://www.googleapis.com/upload/drive/v3/files', size: 500 }); + + expect(res.status).toBe(401); + expect(fetchMock).not.toHaveBeenCalled(); + }); + }); + + describe('POST /api/v1/storage/drive/files/:id/finalize', () => { + it('validates the browser-uploaded file and returns the stored entry', async () => { + connectAccountWithRoots('root-1'); + driveFilesMock.get.mockImplementation(({ fields }: { fields?: string }) => + Promise.resolve( + fields === 'id, parents, trashed' + ? { data: { id: 'file-1', parents: ['root-1'], trashed: false } } + : { + data: { + id: 'file-1', + name: 'big.bin', + size: '1024', + parents: ['root-1'], + webViewLink: 'http://view/file-1', + }, + }, + ), + ); + + const res = await request(app.getHttpServer()) + .post('/api/v1/storage/drive/files/file-1/finalize') + .set('Cookie', accessCookie()); + + expect(res.status).toBe(201); + expect(res.body).toEqual({ + fileId: 'file-1', + fileName: 'big.bin', + size: 1024, + webViewLink: 'http://view/file-1', + }); + }); + + // The browser completes the upload itself, so finalize is where an out-of-tree file must be caught. + it('refuses to finalize a file outside the authorized tree (403)', async () => { + connectAccountWithRoots('root-1'); + driveFilesMock.get.mockResolvedValue({ data: { id: 'file-1', parents: ['elsewhere'] } }); + + const res = await request(app.getHttpServer()) + .post('/api/v1/storage/drive/files/file-1/finalize') + .set('Cookie', accessCookie()); + + expect(res.status).toBe(403); + }); + }); +}); diff --git a/apps/api/tsconfig.json b/apps/api/tsconfig.json index 712047d..5606aa4 100644 --- a/apps/api/tsconfig.json +++ b/apps/api/tsconfig.json @@ -5,7 +5,7 @@ "moduleResolution": "NodeNext", "ignoreDeprecations": "6.0", "target": "ES2025", - "types": ["node", "jest"], + "types": ["node"], "declaration": true, "removeComments": true, "emitDecoratorMetadata": true, @@ -14,6 +14,7 @@ "rootDir": "./src", "outDir": "./dist", "incremental": true, + "tsBuildInfoFile": "./dist/tsconfig.tsbuildinfo", "strictPropertyInitialization": false, "paths": { "@/*": ["./src/*"], diff --git a/apps/web/.env.example b/apps/web/.env.example index 673bd4c..1954306 100644 --- a/apps/web/.env.example +++ b/apps/web/.env.example @@ -1,12 +1,9 @@ # URLs APP_URL="http://localhost:3000" -NEXT_PUBLIC_API_URL="http://localhost:4000/api/v1" -# Optional: server-only API base for server-to-server calls (falls back to NEXT_PUBLIC_API_URL). -# In a compose/prod deploy, point this at the API's internal address, e.g. http://dropto-api:4000/api/v1 -# API_URL="http://localhost:4000/api/v1" +API_URL="http://localhost:4000/api/v1" -# Security -COOKIE_DOMAIN="localhost" +# Branding (optional) — display name of this instance; defaults to "DropTo" when empty. +APP_NAME="DropTo" # Google Picker (browser side) NEXT_PUBLIC_GOOGLE_API_KEY="your_google_picker_api_key" diff --git a/apps/web/Dockerfile b/apps/web/Dockerfile index 0f9a258..d95ef6b 100644 --- a/apps/web/Dockerfile +++ b/apps/web/Dockerfile @@ -1,19 +1,25 @@ # Multi-stage build, run from the monorepo root: # docker build -f apps/web/Dockerfile -t dropto-web . # -# NEXT_PUBLIC_* values are inlined at build time, so they must be passed as -# build args (Coolify: set them as build-time variables). +# NEXT_PUBLIC_* values and APP_URL are inlined at build time, so they must be passed as +# build args (Coolify: set them as build-time variables). APP_NAME is NOT one of them — it is +# read on the server at runtime, so the instance can be rebranded without a rebuild. FROM node:24-alpine AS builder RUN apk add --no-cache libc6-compat WORKDIR /app ENV CI=true +# No .git in the build context, so the root `prepare` (husky) has nothing to install. +ENV HUSKY=0 +ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0 +ENV NEXT_TELEMETRY_DISABLED=1 COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./ COPY apps/web/package.json ./apps/web/ COPY packages/types/package.json ./packages/types/ -RUN npm install -g pnpm +# corepack resolves pnpm from the `packageManager` field, so the image matches the lockfile. +RUN corepack enable RUN pnpm install --frozen-lockfile COPY apps/web ./apps/web @@ -21,12 +27,10 @@ COPY packages/types ./packages/types # Root tsconfig that apps/web/tsconfig.json extends. COPY tsconfig.base.json ./ -ARG NEXT_PUBLIC_API_URL ARG NEXT_PUBLIC_GOOGLE_API_KEY ARG NEXT_PUBLIC_GOOGLE_APP_ID # Public origin, baked into the prerendered SEO metadata (canonical / OpenGraph URLs). ARG APP_URL -ENV NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL ENV NEXT_PUBLIC_GOOGLE_API_KEY=$NEXT_PUBLIC_GOOGLE_API_KEY ENV NEXT_PUBLIC_GOOGLE_APP_ID=$NEXT_PUBLIC_GOOGLE_APP_ID ENV APP_URL=$APP_URL @@ -40,6 +44,7 @@ WORKDIR /app ENV NODE_ENV=production ENV PORT=3000 ENV HOSTNAME=0.0.0.0 +ENV NEXT_TELEMETRY_DISABLED=1 # Standalone output (includes a minimal node_modules + server.js), owned by the non-root user COPY --from=builder --chown=node:node /app/apps/web/.next/standalone ./ @@ -50,4 +55,9 @@ COPY --from=builder --chown=node:node /app/apps/web/public ./apps/web/public USER node EXPOSE 3000 + +# 127.0.0.1, not localhost: that also resolves to ::1, which the IPv4 listener refuses. +HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ + CMD wget -qO- http://127.0.0.1:3000/api/health || exit 1 + CMD ["node", "apps/web/server.js"] diff --git a/apps/web/next.config.ts b/apps/web/next.config.ts index c2616f1..ad5e018 100644 --- a/apps/web/next.config.ts +++ b/apps/web/next.config.ts @@ -4,6 +4,9 @@ import path from 'node:path'; const monorepoRoot = path.join(__dirname, '../..'); const isDevelopment = process.env.NODE_ENV === 'development'; +// https only: from a local http build this would pin `localhost` to https for two years. +const servesHttps = (process.env.APP_URL ?? '').startsWith('https://'); + // Google origins (Picker/OAuth scripts, frames, XHRs, images) allowed by the CSP. const GOOGLE_SCRIPT = 'https://apis.google.com https://*.gstatic.com'; const GOOGLE_FRAME = @@ -36,12 +39,19 @@ const securityHeaders = [ { key: 'X-Frame-Options', value: 'DENY' }, { key: 'X-Content-Type-Options', value: 'nosniff' }, { key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' }, - { key: 'Strict-Transport-Security', value: 'max-age=63072000; includeSubDomains; preload' }, // Drop ambient access to sensor/geolocation APIs the app never uses. { key: 'Permissions-Policy', value: 'camera=(), microphone=(), geolocation=(), browsing-topics=()', }, + ...(servesHttps + ? [ + { + key: 'Strict-Transport-Security', + value: 'max-age=63072000; includeSubDomains; preload', + }, + ] + : []), ]; const nextConfig: NextConfig = { diff --git a/apps/web/package.json b/apps/web/package.json index 33e1fdb..8a91ee3 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,6 +1,5 @@ { "name": "web", - "version": "1.0.0", "private": true, "scripts": { "dev": "next dev -p 3000", @@ -10,10 +9,10 @@ }, "dependencies": { "@dropto/types": "workspace:*", - "axios": "^1.19.0", - "framer-motion": "^12.43.0", - "lucide-react": "^1.28.0", - "next": "16.2.12", + "axios": "^1.20.0", + "lucide-react": "^1.38.0", + "motion": "^13.1.1", + "next": "16.3.3", "next-themes": "^0.4.6", "react": "19.2.8", "react-dom": "19.2.8", @@ -21,10 +20,10 @@ }, "devDependencies": { "@tailwindcss/postcss": "^4.3.3", - "@types/node": "^26.1.2", - "@types/react": "^19.2.17", - "@types/react-dom": "^19.2.3", - "eslint": "^10.8.0", + "@types/node": "^26.4.0", + "@types/react": "^19.2.18", + "@types/react-dom": "^19.2.5", + "eslint": "^10.9.1", "prettier": "^3.9.6", "prettier-plugin-tailwindcss": "^0.8.1", "tailwindcss": "^4.3.3", diff --git a/apps/web/prettier.config.ts b/apps/web/prettier.config.ts index 796cd7c..bae8e33 100644 --- a/apps/web/prettier.config.ts +++ b/apps/web/prettier.config.ts @@ -1,15 +1,10 @@ import type { Config } from 'prettier'; -// Mirrors the root workspace config, adding the Tailwind class-sorting plugin. +import baseConfig from '../../prettier.config.ts'; + +// The workspace config plus the Tailwind class-sorting plugin (web-only). const config: Config = { - tabWidth: 2, - printWidth: 100, - endOfLine: 'auto', - arrowParens: 'always', - semi: true, - singleQuote: true, - jsxSingleQuote: true, - bracketSameLine: true, + ...baseConfig, plugins: ['prettier-plugin-tailwindcss'], }; diff --git a/apps/web/src/actions/auth/auth.actions.ts b/apps/web/src/actions/auth/auth.actions.ts index 571b73e..ef0c4a4 100644 --- a/apps/web/src/actions/auth/auth.actions.ts +++ b/apps/web/src/actions/auth/auth.actions.ts @@ -14,10 +14,6 @@ import { import { applyAuthCookies, parseAuthSetCookies } from '@/common/services/auth/tokens.server'; import { getHttp } from '@/common/services/axios/axios.instance'; import { type ActionResult, extractApiError, runAction } from '@/common/utils/action.functions'; -import { appServerConfig } from '@/configs/app/app.server-config'; - -/** Owner-proof cookie lifetime — mirrors the API's DRIVE_OWNER_TTL_MS (30 days). */ -const DRIVE_OWNER_MAX_AGE_S = 30 * 24 * 60 * 60; /** Outcome of a sign-in attempt. */ export interface LoginResult { @@ -55,22 +51,6 @@ export async function login(username: string, password: string): Promise { - const { isProduction } = appServerConfig.nodeEnv; - - (await cookies()).set(DRIVE_OWNER_COOKIE, token, { - httpOnly: true, - secure: isProduction, - sameSite: 'lax', - path: '/', - domain: isProduction ? appServerConfig.cookieDomain : undefined, - maxAge: DRIVE_OWNER_MAX_AGE_S, - }); -} - /** * Clears the Drive owner-proof cookie (on disconnect). **/ diff --git a/apps/web/src/actions/storage/storage.actions.ts b/apps/web/src/actions/storage/storage.actions.ts index 721e89d..34b980b 100644 --- a/apps/web/src/actions/storage/storage.actions.ts +++ b/apps/web/src/actions/storage/storage.actions.ts @@ -5,10 +5,10 @@ import type { DriveEntryPage, ListContentsQuery, ResumableUploadSession, + ResumableUploadStatus, StorageBackend, StorageStatus, UploadResult, - UploadStatus, } from '@dropto/types'; import { @@ -92,7 +92,7 @@ export async function uploadStatusAction( backend: StorageBackend, uploadUrl: string, size: number, -): Promise> { +): Promise> { return runAction(() => getUploadStatus(backend, uploadUrl, size)); } diff --git a/apps/web/src/app/(auth)/login/page.tsx b/apps/web/src/app/(auth)/login/page.tsx index 8cea774..b9102c0 100644 --- a/apps/web/src/app/(auth)/login/page.tsx +++ b/apps/web/src/app/(auth)/login/page.tsx @@ -1,6 +1,7 @@ import type { Metadata } from 'next'; import LoginForm from '@/components/forms/LoginForm'; +import { appServerConfig } from '@/configs/app/app.server-config'; export const metadata: Metadata = { title: 'Sign in', @@ -10,5 +11,5 @@ export const metadata: Metadata = { * Login route — the proxy redirects already-authenticated visitors to `/`. **/ export default function LoginPage() { - return ; + return ; } diff --git a/apps/web/src/app/(workspace)/[backend]/[[...ids]]/page.tsx b/apps/web/src/app/(workspace)/[backend]/[[...ids]]/page.tsx index df68647..0022375 100644 --- a/apps/web/src/app/(workspace)/[backend]/[[...ids]]/page.tsx +++ b/apps/web/src/app/(workspace)/[backend]/[[...ids]]/page.tsx @@ -1,22 +1,30 @@ import type { StorageBackend, StorageStatus } from '@dropto/types'; import type { Metadata } from 'next'; -import { redirect } from 'next/navigation'; +import { notFound, redirect } from 'next/navigation'; import { getStatuses, resolveNames } from '@/common/services/api/storage.api'; import { getCurrentUser } from '@/common/services/auth/current-user'; import type { Crumb } from '@/common/types/workspace.types'; import { slugify } from '@/common/utils/storage-url'; -import NotFoundContent from '@/components/layouts/NotFoundContent'; import WorkspaceClient from '@/components/layouts/WorkspaceClient'; - -export const metadata: Metadata = { - title: 'Workspace', -}; +import { appServerConfig } from '@/configs/app/app.server-config'; interface Props { params: Promise<{ backend: string; ids?: string[] }>; } +/** + * Title for the route — "Page not found" when the URL names no backend. + **/ +// Dynamic: the client re-applies this segment's metadata after hydration and would win the tab title back on a 404. +export async function generateMetadata({ params }: Props): Promise { + const { backend } = await params; + + return backend === 'drive' || backend === 's3' + ? { title: 'Workspace' } + : { title: 'Page not found', robots: { index: false, follow: true } }; +} + /** * Workspace route — resolves the browse location (and breadcrumb) from the URL, server-side. **/ @@ -24,9 +32,9 @@ export default async function WorkspaceBrowsePage({ params }: Props) { const user = await getCurrentUser(); const { backend, ids } = await params; - // Unknown backend → full-page 404; rendered inline (not notFound()) to avoid the next-themes script-tag warning. + // A URL naming no backend is not a workspace, so it gets the real 404; an unresolved folder stays in the workspace. if (backend !== 'drive' && backend !== 's3') { - return ; + notFound(); } const restIds = (ids ?? []).slice(1); @@ -72,6 +80,7 @@ export default async function WorkspaceBrowsePage({ params }: Props) { return ( { + const origin = resolveRequestOrigin(request); + const query = new URL(request.url).search; + const state = (await cookies()).get(OAUTH_STATE_COOKIE)?.value; + + return proxyOAuthLeg(`/google-auth/google/callback${query}`, { + cookieHeader: state ? `${OAUTH_STATE_COOKIE}=${state}` : null, + rebaseOn: origin, + // The API can only pass the owner proof in the query, so it is claimed here and never reaches the address bar. + transformDestination: (destination) => { + const ownerToken = destination.searchParams.get('ownerToken'); + + destination.searchParams.delete('ownerToken'); + destination.searchParams.delete('email'); + + return { + url: destination, + cookies: ownerToken + ? [ + { + name: DRIVE_OWNER_COOKIE, + value: ownerToken, + options: { + httpOnly: true, + secure: appServerConfig.nodeEnv.isProduction, + sameSite: 'lax' as const, + path: '/', + maxAge: DRIVE_OWNER_MAX_AGE_S, + }, + }, + ] + : [], + }; + }, + fallbackUrl: origin, + unauthorizedUrl: `${origin}/login`, + }); +} diff --git a/apps/web/src/app/api/oauth/google/route.ts b/apps/web/src/app/api/oauth/google/route.ts new file mode 100644 index 0000000..57580de --- /dev/null +++ b/apps/web/src/app/api/oauth/google/route.ts @@ -0,0 +1,31 @@ +import { NextResponse } from 'next/server'; + +import { + proxyOAuthLeg, + resolveSessionForPassthrough, +} from '@/common/services/api/passthrough.server'; +import { isCrossSiteRequest, resolveRequestOrigin } from '@/common/utils/request-origin'; + +/** Consent screens Google may send the operator to. */ +const GOOGLE_CONSENT_HOSTS = new Set(['accounts.google.com']); + +/** + * Starts the Google consent flow, so the browser never has to reach the API's own host. + **/ +export async function GET(request: Request): Promise { + if (isCrossSiteRequest(request)) { + return NextResponse.json({ message: 'Cross-site request rejected.' }, { status: 403 }); + } + + const origin = resolveRequestOrigin(request); + const session = await resolveSessionForPassthrough(); + + return proxyOAuthLeg('/google-auth/google', { + cookieHeader: session.cookieHeader, + rotated: session.rotated, + isAllowedTarget: (target) => + target.protocol === 'https:' && GOOGLE_CONSENT_HOSTS.has(target.hostname), + fallbackUrl: origin, + unauthorizedUrl: `${origin}/login`, + }); +} diff --git a/apps/web/src/app/api/og/route.tsx b/apps/web/src/app/api/og/route.tsx index b50ef36..a118f61 100644 --- a/apps/web/src/app/api/og/route.tsx +++ b/apps/web/src/app/api/og/route.tsx @@ -1,5 +1,10 @@ import { ImageResponse } from 'next/og'; +import { metadataConfig } from '@/configs/seo/metadata.config'; + +const wordmark = metadataConfig.title; +const subtitle = metadataConfig.tagline; + /** * Loads a Poppins weight from Google Fonts as TTF/OTF data for Satori (`text` subsets it). **/ @@ -16,10 +21,10 @@ async function loadPoppins(weight: number, text: string): Promise { } /** - * OG image (`GET /api/og`) — the DropTo badge + wordmark on a light zinc background. + * OG image (`GET /api/og`) — the badge, the wordmark and the tagline on a light zinc background. **/ export async function GET() { - const poppins = await loadPoppins(600, 'DropTo'); + const poppins = await loadPoppins(600, `${wordmark}${subtitle}`); return new ImageResponse(
- DropTo + {wordmark} +
+ +
+ {subtitle}
, diff --git a/apps/web/src/app/globals.css b/apps/web/src/app/globals.css index 384baf6..5273c48 100644 --- a/apps/web/src/app/globals.css +++ b/apps/web/src/app/globals.css @@ -64,3 +64,49 @@ @apply cursor-not-allowed opacity-50; } } + +/* Wraps the app so the JavaScript-off screen can hide it with a single rule; generates no box. */ +.app-shell { + display: contents; +} + +/* JavaScript-off screen — themed with `prefers-color-scheme`, as next-themes never adds `.dark`. */ +.noscript-screen { + display: flex; + min-height: 100vh; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 1rem; + padding: 0 1rem; + text-align: center; +} + +.noscript-eyebrow { + font-size: 0.875rem; + font-weight: 600; + letter-spacing: 0.1em; + text-transform: uppercase; + color: var(--color-green-600); +} + +.noscript-title { + font-size: 1.5rem; + font-weight: 700; + color: var(--color-zinc-950); +} + +.noscript-text { + max-width: 28rem; + color: var(--color-zinc-600); +} + +@media (prefers-color-scheme: dark) { + .noscript-title { + color: var(--color-zinc-50); + } + + .noscript-text { + color: var(--color-zinc-400); + } +} diff --git a/apps/web/src/app/layout.tsx b/apps/web/src/app/layout.tsx index 5117bf7..82c6666 100644 --- a/apps/web/src/app/layout.tsx +++ b/apps/web/src/app/layout.tsx @@ -65,14 +65,34 @@ export default function RootLayout({ children }: Readonly) { return ( - + - - {children} - +
+ + {children} + +
+ + ); diff --git a/apps/web/src/app/logout/route.ts b/apps/web/src/app/logout/route.ts index d510de7..ee41382 100644 --- a/apps/web/src/app/logout/route.ts +++ b/apps/web/src/app/logout/route.ts @@ -1,6 +1,10 @@ import { NextRequest, NextResponse } from 'next/server'; -import { clearAuthCookies } from '@/common/services/auth/tokens.server'; +import { + applyAuthCookies, + clearAuthCookies, + parseAuthSetCookies, +} from '@/common/services/auth/tokens.server'; import { getHttp } from '@/common/services/axios/axios.instance'; import { isCrossSiteRequest, resolveRequestOrigin } from '@/common/utils/request-origin'; @@ -15,15 +19,27 @@ export async function GET(request: NextRequest): Promise { return NextResponse.redirect(new URL('/login', origin)); } + let cleared: ReturnType = []; + try { const http = await getHttp(); - await http.post('/auth/logout'); + const { headers } = await http.post('/auth/logout'); + + // The API's own clearing cookies carry the Domain it set them with — forwarding them is what makes the delete land. + cleared = parseAuthSetCookies( + Array.isArray(headers['set-cookie']) ? headers['set-cookie'] : undefined, + ); } catch { - // Clear cookies locally regardless. + // Unreachable API — fall through and clear locally below. } const response = NextResponse.redirect(new URL('/login', origin)); - clearAuthCookies(response.cookies); + + if (cleared.length > 0) { + applyAuthCookies(response.cookies, cleared); + } else { + clearAuthCookies(response.cookies); + } return response; } diff --git a/apps/web/src/app/manifest.ts b/apps/web/src/app/manifest.ts index be1c327..d95a2de 100644 --- a/apps/web/src/app/manifest.ts +++ b/apps/web/src/app/manifest.ts @@ -2,6 +2,9 @@ import type { MetadataRoute } from 'next'; import { metadataConfig } from '@/configs/seo/metadata.config'; +/** The name comes from the runtime env, so this must not be prerendered at build time. */ +export const dynamic = 'force-dynamic'; + /** * Web app manifest (PWA); icons live in `public/`. **/ diff --git a/apps/web/src/app/not-found.tsx b/apps/web/src/app/not-found.tsx index bbcf039..fd258eb 100644 --- a/apps/web/src/app/not-found.tsx +++ b/apps/web/src/app/not-found.tsx @@ -4,6 +4,7 @@ import NotFoundContent from '@/components/layouts/NotFoundContent'; export const metadata: Metadata = { title: 'Page not found', + robots: { index: false, follow: true }, }; /** diff --git a/apps/web/src/common/constants/auth.constants.ts b/apps/web/src/common/constants/auth.constants.ts index 6a2dfde..2074eb5 100644 --- a/apps/web/src/common/constants/auth.constants.ts +++ b/apps/web/src/common/constants/auth.constants.ts @@ -3,6 +3,10 @@ export const ACCESS_TOKEN_COOKIE = 'accessToken'; export const REFRESH_TOKEN_COOKIE = 'refreshToken'; export const REFRESH_LOCK_COOKIE = 'refresh_lock'; export const DRIVE_OWNER_COOKIE = 'driveOwner'; +export const OAUTH_STATE_COOKIE = 'oauthState'; + +/** How long the Drive owner-proof stays valid in the browser. */ +export const DRIVE_OWNER_MAX_AGE_S = 30 * 24 * 60 * 60; /** Refresh proactively when the access token expires within this window. */ export const ACCESS_EXP_SKEW_MS = 60_000; diff --git a/apps/web/src/common/constants/layout.constants.ts b/apps/web/src/common/constants/layout.constants.ts new file mode 100644 index 0000000..3b6a2b7 --- /dev/null +++ b/apps/web/src/common/constants/layout.constants.ts @@ -0,0 +1,5 @@ +/** + * Viewport at which the desktop chrome fits: the sidebar rail and the split view. Tailwind's `md`. + **/ +// Below it the sidebar is a swipe drawer and the split view is gone — no room, and its drag & drop is mouse-only. +export const DESKTOP_QUERY = '(min-width: 48rem)'; diff --git a/apps/web/src/common/hooks/useBrowsePane.ts b/apps/web/src/common/hooks/useBrowsePane.ts index 461ce95..78cf152 100644 --- a/apps/web/src/common/hooks/useBrowsePane.ts +++ b/apps/web/src/common/hooks/useBrowsePane.ts @@ -3,22 +3,12 @@ import type { StorageBackend } from '@dropto/types'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { listContentsAction } from '@/actions/storage/storage.actions'; import { useDebouncedValue } from '@/common/hooks/useDebouncedValue'; +import { type PaneError, useEntryListing } from '@/common/hooks/useEntryListing'; +import { useEntrySelection } from '@/common/hooks/useEntrySelection'; import type { Crumb, SortDir, SortKey, ViewEntry } from '@/common/types/workspace.types'; -import { toViewEntries } from '@/common/utils/view-entry.functions'; -/** - * Drive filters by name server-side; S3 lists a whole level and is filtered client-side. - **/ -const serverSearch = (backend: StorageBackend | null, term: string): string | undefined => - backend === 'drive' && term ? term : undefined; - -/** A failed read, surfaced to the pane's error handler. */ -export interface PaneError { - error?: string; - status?: number; -} +export type { PaneError }; /** A single independently-browsable pane of folders/files. */ export interface BrowsePane { @@ -52,42 +42,42 @@ export interface BrowsePane { /** * Owns one browsing pane's state (path, entries, loading, multi-selection). **/ +// Location lives in local state here (the main pane uses the URL); everything below it is shared. export function useBrowsePane( backend: StorageBackend | null, roots: ViewEntry[], onError: (error: PaneError) => void, ): BrowsePane { const [path, setPath] = useState([]); - const [entries, setEntries] = useState([]); - const [loading, setLoading] = useState(false); - const [selectedIds, setSelectedIds] = useState>(new Set()); const [sortKey, setSortKey] = useState('name'); const [sortDir, setSortDir] = useState('asc'); const [search, setSearch] = useState(''); - const [nextPageToken, setNextPageToken] = useState(null); - const [loadingMore, setLoadingMore] = useState(false); const currentFolderId = path.length > 0 ? path[path.length - 1].id : null; const atRoots = path.length === 0; const debouncedSearch = useDebouncedValue(search.trim(), 250); - // Bumped on every listing read; a resolved request whose id no longer matches is stale (the pane - // navigated away mid-flight) and must not paint over the current folder. - const listSeq = useRef(0); - useEffect(() => { setPath([]); }, [backend]); - useEffect(() => { - setSelectedIds(new Set()); - }, [currentFolderId, backend]); - // A new folder starts unfiltered. useEffect(() => { setSearch(''); }, [currentFolderId, backend]); + const listing = useEntryListing({ + backend, + currentFolderId, + roots, + search: debouncedSearch, + sortKey, + sortDir, + onError, + }); + + const selection = useEntrySelection(currentFolderId, backend); + // Keep the latest sortKey for toggleSort without making it depend on (and re-create) on every change. const sortKeyRef = useRef(sortKey); sortKeyRef.current = sortKey; @@ -97,70 +87,6 @@ export function useBrowsePane( setSortKey(key); }, []); - const reload = useCallback(async () => { - const seq = ++listSeq.current; - - if (currentFolderId === null || backend === null) { - setEntries(roots); - setNextPageToken(null); - setLoading(false); - return; - } - - setLoading(true); - const result = await listContentsAction(backend, currentFolderId, { - search: serverSearch(backend, debouncedSearch), - sortKey, - sortDir, - }); - - // Superseded while in flight — a newer read owns the pane now. - if (seq !== listSeq.current) { - return; - } - - if (result.ok) { - setEntries(toViewEntries(result.data?.entries ?? [])); - setNextPageToken(result.data?.nextPageToken ?? null); - } else { - onError({ error: result.error, status: result.status }); - } - setLoading(false); - }, [backend, currentFolderId, roots, onError, debouncedSearch, sortKey, sortDir]); - - useEffect(() => { - void reload(); - }, [reload]); - - const loadMore = useCallback(async () => { - if (nextPageToken === null || backend === null || currentFolderId === null || loadingMore) { - return; - } - - const seq = listSeq.current; - setLoadingMore(true); - const result = await listContentsAction(backend, currentFolderId, { - pageToken: nextPageToken, - search: serverSearch(backend, debouncedSearch), - sortKey, - sortDir, - }); - - // The folder changed under us — appending this page would mix two listings. - if (seq !== listSeq.current) { - setLoadingMore(false); - return; - } - - if (result.ok) { - setEntries((current) => [...current, ...toViewEntries(result.data?.entries ?? [])]); - setNextPageToken(result.data?.nextPageToken ?? null); - } else { - onError({ error: result.error, status: result.status }); - } - setLoadingMore(false); - }, [nextPageToken, backend, currentFolderId, loadingMore, debouncedSearch, sortKey, sortDir, onError]); - const openFolder = useCallback((entry: ViewEntry) => { setPath((current) => [ ...current, @@ -174,43 +100,19 @@ export function useBrowsePane( const goTo = useCallback((next: Crumb[]) => setPath(next), []); - const toggleSelect = useCallback((id: string) => { - setSelectedIds((current) => { - const next = new Set(current); - if (next.has(id)) { - next.delete(id); - } else { - next.add(id); - } - return next; - }); - }, []); - - const selectAll = useCallback(() => { - setSelectedIds(new Set(entries.map((entry) => entry.id))); - }, [entries]); - - const clearSelection = useCallback(() => setSelectedIds(new Set()), []); - - const setSelection = useCallback((ids: string[]) => setSelectedIds(new Set(ids)), []); - - const pruneSelection = useCallback((id: string) => { - setSelectedIds((current) => { - if (!current.has(id)) { - return current; - } - const next = new Set(current); - next.delete(id); - return next; - }); - }, []); + const { entries } = listing; + const { selectAll: selectIds } = selection; + const selectAll = useCallback( + () => selectIds(entries.map((entry) => entry.id)), + [entries, selectIds], + ); return useMemo( () => ({ path, - entries, - loading, - selectedIds, + entries: listing.entries, + loading: listing.loading, + selectedIds: selection.selectedIds, currentFolderId, atRoots, sortKey, @@ -218,42 +120,33 @@ export function useBrowsePane( toggleSort, search, setSearch, - hasMore: nextPageToken !== null, - loadingMore, - loadMore, + hasMore: listing.hasMore, + loadingMore: listing.loadingMore, + loadMore: listing.loadMore, openFolder, navigate, goTo, - reload, - toggleSelect, + reload: listing.reload, + toggleSelect: selection.toggleSelect, selectAll, - clearSelection, - setSelection, - pruneSelection, + clearSelection: selection.clearSelection, + setSelection: selection.setSelection, + pruneSelection: selection.pruneSelection, }), [ path, - entries, - loading, - selectedIds, + listing, + selection, currentFolderId, atRoots, sortKey, sortDir, toggleSort, search, - nextPageToken, - loadingMore, - loadMore, openFolder, navigate, goTo, - reload, - toggleSelect, selectAll, - clearSelection, - setSelection, - pruneSelection, ], ); } diff --git a/apps/web/src/common/hooks/useEntryListing.ts b/apps/web/src/common/hooks/useEntryListing.ts new file mode 100644 index 0000000..82b660a --- /dev/null +++ b/apps/web/src/common/hooks/useEntryListing.ts @@ -0,0 +1,145 @@ +'use client'; + +import type { StorageBackend } from '@dropto/types'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; + +import { listContentsAction } from '@/actions/storage/storage.actions'; +import type { SortDir, SortKey, ViewEntry } from '@/common/types/workspace.types'; +import { toViewEntries } from '@/common/utils/view-entry.functions'; + +/** A failed read, surfaced to the pane's error handler. */ +export interface PaneError { + error?: string; + status?: number; +} + +/** Where and how a pane wants its entries listed. */ +interface ListingOptions { + backend: StorageBackend | null; + /** Null at the roots level, where `roots` is shown instead of a server listing. */ + currentFolderId: string | null; + roots: ViewEntry[]; + /** Already debounced by the caller. */ + search: string; + sortKey: SortKey; + sortDir: SortDir; + onError: (error: PaneError) => void; +} + +/** One pane's listing: the entries plus their loading and pagination state. */ +export interface EntryListing { + entries: ViewEntry[]; + setEntries: (update: (current: ViewEntry[]) => ViewEntry[]) => void; + loading: boolean; + /** Whether another page can be loaded (infinite scroll). */ + hasMore: boolean; + loadingMore: boolean; + loadMore: () => Promise; + reload: () => Promise; +} + +/** + * Drive filters by name server-side; S3 lists a whole level and is filtered client-side. + **/ +const serverSearch = (backend: StorageBackend | null, term: string): string | undefined => + backend === 'drive' && term ? term : undefined; + +/** + * Loads one pane's entries, with cursor pagination and stale-response protection. + **/ +// Shared by both panes; they differ only in where the folder and sort come from, never in how a listing is read. +export function useEntryListing({ + backend, + currentFolderId, + roots, + search, + sortKey, + sortDir, + onError, +}: ListingOptions): EntryListing { + const [entries, setEntries] = useState([]); + const [loading, setLoading] = useState(false); + const [nextPageToken, setNextPageToken] = useState(null); + const [loadingMore, setLoadingMore] = useState(false); + + // A resolved request whose id no longer matches is stale and must not paint over the current folder. + const listSeq = useRef(0); + + const reload = useCallback(async () => { + const seq = ++listSeq.current; + + if (currentFolderId === null || backend === null) { + setEntries(roots); + setNextPageToken(null); + setLoading(false); + return; + } + + setLoading(true); + const result = await listContentsAction(backend, currentFolderId, { + search: serverSearch(backend, search), + sortKey, + sortDir, + }); + + // Superseded while in flight — a newer read owns the pane now. + if (seq !== listSeq.current) { + return; + } + + if (result.ok) { + setEntries(toViewEntries(result.data?.entries ?? [])); + setNextPageToken(result.data?.nextPageToken ?? null); + } else { + onError({ error: result.error, status: result.status }); + } + setLoading(false); + }, [backend, currentFolderId, roots, onError, search, sortKey, sortDir]); + + useEffect(() => { + void reload(); + }, [reload]); + + const loadMore = useCallback(async () => { + if (nextPageToken === null || backend === null || currentFolderId === null || loadingMore) { + return; + } + + const seq = listSeq.current; + setLoadingMore(true); + const result = await listContentsAction(backend, currentFolderId, { + pageToken: nextPageToken, + search: serverSearch(backend, search), + sortKey, + sortDir, + }); + + // The folder changed under us — appending this page would mix two listings. + if (seq !== listSeq.current) { + setLoadingMore(false); + return; + } + + if (result.ok) { + // Append (never replace) so the scroll position is preserved during infinite scroll. + setEntries((current) => [...current, ...toViewEntries(result.data?.entries ?? [])]); + setNextPageToken(result.data?.nextPageToken ?? null); + } else { + onError({ error: result.error, status: result.status }); + } + setLoadingMore(false); + }, [nextPageToken, backend, currentFolderId, loadingMore, search, sortKey, sortDir, onError]); + + return useMemo( + () => ({ + entries, + setEntries, + loading, + hasMore: nextPageToken !== null, + loadingMore, + loadMore, + reload, + }), + [entries, loading, nextPageToken, loadingMore, loadMore, reload], + ); +} diff --git a/apps/web/src/common/hooks/useEntryOperations.ts b/apps/web/src/common/hooks/useEntryOperations.ts new file mode 100644 index 0000000..b9c39cb --- /dev/null +++ b/apps/web/src/common/hooks/useEntryOperations.ts @@ -0,0 +1,352 @@ +'use client'; + +import type { StorageBackend } from '@dropto/types'; +import { type FormEvent, useCallback, useState } from 'react'; + +import { removeFolderAction } from '@/actions/auth/auth.actions'; +import { + createFolderAction, + deleteItemAction, + renameItemAction, +} from '@/actions/storage/storage.actions'; +import type { ViewEntry } from '@/common/types/workspace.types'; +import { extractApiErrorMessage } from '@/common/utils/error.functions'; +import { useToast } from '@/components/providers/ToastProvider'; + +/** Which pane an operation targets. */ +type Pane = 0 | 1; + +/** A pending extension change awaiting confirmation (the "modal on a modal"). */ +export interface ExtensionWarning { + /** The name exactly as typed (keeps the new extension). */ + use: string; + /** The typed base name but with the original extension kept. */ + keep: string; + /** The original extension (`''` when the file had none). */ + fromExt: string; + /** The new extension (`''` when the new name has none). */ + toExt: string; +} + +/** How the operations reach whichever panes the workspace is showing. */ +interface PaneAccess { + folderId: (pane: Pane) => string | null; + selectedIds: (pane: Pane) => string[]; + clearSelection: (pane: Pane) => void; +} + +interface Options { + backend: StorageBackend | null; + panes: PaneAccess; + /** Refreshes both panes after a mutation. */ + reloadPanes: () => Promise; + /** Refreshes the sidebar (a parent folder or root may have disappeared). */ + loadStatus: () => Promise; + /** Drops ids from the preview and both panes' selection after they are gone or renamed. */ + forgetEntries: (ids: string[]) => void; +} + +/** + * Splits a name into base + extension (leading/trailing dot = no extension). + **/ +const splitExtension = (name: string): { base: string; ext: string } => { + const dot = name.lastIndexOf('.'); + + if (dot <= 0 || dot === name.length - 1) { + return { base: name, ext: '' }; + } + + return { base: name.slice(0, dot), ext: name.slice(dot) }; +}; + +/** + * Every mutation on an entry, together with the dialog state that confirms it. + **/ +// All four mutations follow the same shape (guard → act → reload → forget → close), so they belong together. +export function useEntryOperations({ + backend, + panes, + reloadPanes, + loadStatus, + forgetEntries, +}: Options) { + const toast = useToast(); + + const [newFolderOpen, setNewFolderOpen] = useState(false); + const [newFolderName, setNewFolderName] = useState(''); + const [newFolderPane, setNewFolderPane] = useState(0); + const [creating, setCreating] = useState(false); + + const [deleteTarget, setDeleteTarget] = useState(null); + const [deleting, setDeleting] = useState(false); + + const [bulkOpen, setBulkOpen] = useState(false); + const [bulkPane, setBulkPane] = useState(0); + const [bulkDeleting, setBulkDeleting] = useState(false); + + const [renameTarget, setRenameTarget] = useState(null); + const [renameName, setRenameName] = useState(''); + const [renaming, setRenaming] = useState(false); + const [pendingRename, setPendingRename] = useState(null); + const [extWarning, setExtWarning] = useState(null); + + const [unselectTarget, setUnselectTarget] = useState(null); + const [unselecting, setUnselecting] = useState(false); + + // --- New folder --------------------------------------------------------------- + + const openNewFolder = useCallback((pane: Pane) => { + setNewFolderPane(pane); + setNewFolderOpen(true); + }, []); + + const submitNewFolder = useCallback( + async (event: FormEvent) => { + event.preventDefault(); + const name = newFolderName.trim(); + const targetFolderId = panes.folderId(newFolderPane); + + if (targetFolderId === null || backend === null || !name || creating) { + return; + } + + setCreating(true); + try { + const result = await createFolderAction(backend, targetFolderId, name); + + if (!result.ok) { + toast.error(result.error ?? 'Something went wrong.'); + // The parent is gone (deleted outside the app) — drop it from the sidebar roots. + if (result.status === 424 || result.status === 404) { + void loadStatus(); + } + return; + } + + await reloadPanes(); + toast.success(`Folder "${name}" created.`); + setNewFolderOpen(false); + setNewFolderName(''); + } catch (error) { + toast.error(extractApiErrorMessage(error)); + } finally { + setCreating(false); + } + }, + [backend, panes, newFolderPane, newFolderName, creating, reloadPanes, toast, loadStatus], + ); + + // --- Delete ------------------------------------------------------------------- + + const confirmDelete = useCallback(async () => { + if (!deleteTarget || backend === null) { + return; + } + const entry = deleteTarget; + + setDeleting(true); + try { + const result = await deleteItemAction(backend, entry.id); + + if (!result.ok) { + toast.error(result.error ?? 'Something went wrong.'); + return; + } + + await reloadPanes(); + forgetEntries([entry.id]); + toast.success(`${entry.isFolder ? 'Folder' : 'File'} "${entry.name}" deleted.`); + setDeleteTarget(null); + } catch (error) { + toast.error(extractApiErrorMessage(error)); + } finally { + setDeleting(false); + } + }, [backend, deleteTarget, reloadPanes, forgetEntries, toast]); + + const openBulkDelete = useCallback((pane: Pane) => { + setBulkPane(pane); + setBulkOpen(true); + }, []); + + const confirmBulkDelete = useCallback(async () => { + const ids = panes.selectedIds(bulkPane); + + if (backend === null || ids.length === 0) { + return; + } + + setBulkDeleting(true); + try { + const results = await Promise.all(ids.map((id) => deleteItemAction(backend, id))); + const failed = results.filter((result) => !result.ok).length; + + await reloadPanes(); + forgetEntries(ids); + panes.clearSelection(bulkPane); + setBulkOpen(false); + + if (failed > 0) { + toast.error(`Failed to delete ${failed} item${failed === 1 ? '' : 's'}.`); + } else { + toast.success(`Deleted ${ids.length} item${ids.length === 1 ? '' : 's'}.`); + } + } catch (error) { + toast.error(extractApiErrorMessage(error)); + } finally { + setBulkDeleting(false); + } + }, [backend, bulkPane, panes, reloadPanes, forgetEntries, toast]); + + // --- Rename ------------------------------------------------------------------- + + const openRename = useCallback((entry: ViewEntry) => { + setRenameTarget(entry); + setRenameName(entry.name); + }, []); + + const runRename = useCallback( + async (name: string) => { + if (!renameTarget || backend === null) { + return; + } + + // Renaming to the same name is a no-op (and an S3 self-delete) — guard it. + if (name === renameTarget.name) { + setRenameTarget(null); + setExtWarning(null); + return; + } + + setRenaming(true); + setPendingRename(name); + try { + const result = await renameItemAction(backend, renameTarget.id, name); + + if (!result.ok) { + toast.error(result.error ?? 'Something went wrong.'); + return; + } + + await reloadPanes(); + // The id can change on rename (S3 keys), so drop the old id from selection/preview. + forgetEntries([renameTarget.id]); + toast.success(`Renamed to "${name}".`); + setRenameTarget(null); + setExtWarning(null); + } catch (error) { + toast.error(extractApiErrorMessage(error)); + } finally { + setRenaming(false); + setPendingRename(null); + } + }, + [backend, renameTarget, reloadPanes, forgetEntries, toast], + ); + + const submitRename = useCallback( + (event: FormEvent) => { + event.preventDefault(); + const name = renameName.trim(); + + if (!renameTarget || backend === null || !name || renaming) { + return; + } + if (name === renameTarget.name) { + setRenameTarget(null); + return; + } + + // Warn (Finder-style) before changing a file's extension. Folders have none. + if (!renameTarget.isFolder) { + const fromExt = splitExtension(renameTarget.name).ext; + const { base, ext: toExt } = splitExtension(name); + + if (fromExt.toLowerCase() !== toExt.toLowerCase()) { + setExtWarning({ use: name, keep: `${base}${fromExt}`, fromExt, toExt }); + return; + } + } + + void runRename(name); + }, + [renameName, renameTarget, backend, renaming, runRename], + ); + + // --- Unauthorize a Drive root ------------------------------------------------- + + const confirmUnselect = useCallback(async () => { + if (!unselectTarget) { + return; + } + + setUnselecting(true); + try { + const result = await removeFolderAction(unselectTarget.id); + + if (!result.ok) { + toast.error(result.error ?? 'Something went wrong.'); + return; + } + + setUnselectTarget(null); + await loadStatus(); + toast.success('Folder removed from the app.'); + } catch (error) { + toast.error(extractApiErrorMessage(error)); + } finally { + setUnselecting(false); + } + }, [unselectTarget, loadStatus, toast]); + + return { + newFolder: { + open: newFolderOpen, + name: newFolderName, + creating, + setName: setNewFolderName, + openFor: openNewFolder, + close: () => setNewFolderOpen(false), + submit: submitNewFolder, + }, + remove: { + target: deleteTarget, + deleting, + request: setDeleteTarget, + cancel: () => setDeleteTarget(null), + confirm: confirmDelete, + }, + bulkRemove: { + open: bulkOpen, + pane: bulkPane, + deleting: bulkDeleting, + count: panes.selectedIds(bulkPane).length, + openFor: openBulkDelete, + close: () => setBulkOpen(false), + confirm: confirmBulkDelete, + }, + rename: { + target: renameTarget, + name: renameName, + renaming, + pending: pendingRename, + extWarning, + setName: setRenameName, + open: openRename, + close: () => setRenameTarget(null), + submit: submitRename, + run: runRename, + dismissExtWarning: () => setExtWarning(null), + }, + unselectRoot: { + target: unselectTarget, + busy: unselecting, + request: setUnselectTarget, + cancel: () => setUnselectTarget(null), + confirm: confirmUnselect, + }, + }; +} + +/** Everything the workspace dialogs need to render and drive themselves. */ +export type EntryOperations = ReturnType; diff --git a/apps/web/src/common/hooks/useEntrySelection.ts b/apps/web/src/common/hooks/useEntrySelection.ts new file mode 100644 index 0000000..b1f8ce3 --- /dev/null +++ b/apps/web/src/common/hooks/useEntrySelection.ts @@ -0,0 +1,69 @@ +'use client'; + +import type { StorageBackend } from '@dropto/types'; +import { useCallback, useEffect, useMemo, useState } from 'react'; + +/** Multi-selection over the entries of one pane. */ +export interface EntrySelection { + selectedIds: Set; + toggleSelect: (id: string) => void; + selectAll: (ids: string[]) => void; + clearSelection: () => void; + setSelection: (ids: string[]) => void; + /** Drops a single id (an entry that was deleted or moved away). */ + pruneSelection: (id: string) => void; +} + +/** + * Owns a pane's multi-selection, resetting it whenever the pane changes folder or backend. + **/ +// Shared by both panes: the split pane and the URL-driven main pane had identical copies of this. +export function useEntrySelection( + currentFolderId: string | null, + backend: StorageBackend | null, +): EntrySelection { + const [selectedIds, setSelectedIds] = useState>(new Set()); + + // A different folder (or storage) means a different set of entries — the old ids are meaningless. + useEffect(() => { + setSelectedIds(new Set()); + }, [currentFolderId, backend]); + + const toggleSelect = useCallback((id: string) => { + setSelectedIds((current) => { + const next = new Set(current); + + if (next.has(id)) { + next.delete(id); + } else { + next.add(id); + } + + return next; + }); + }, []); + + const selectAll = useCallback((ids: string[]) => setSelectedIds(new Set(ids)), []); + + const clearSelection = useCallback(() => setSelectedIds(new Set()), []); + + const setSelection = useCallback((ids: string[]) => setSelectedIds(new Set(ids)), []); + + const pruneSelection = useCallback((id: string) => { + setSelectedIds((current) => { + if (!current.has(id)) { + return current; + } + + const next = new Set(current); + next.delete(id); + + return next; + }); + }, []); + + return useMemo( + () => ({ selectedIds, toggleSelect, selectAll, clearSelection, setSelection, pruneSelection }), + [selectedIds, toggleSelect, selectAll, clearSelection, setSelection, pruneSelection], + ); +} diff --git a/apps/web/src/common/hooks/useMediaQuery.ts b/apps/web/src/common/hooks/useMediaQuery.ts new file mode 100644 index 0000000..fe6e428 --- /dev/null +++ b/apps/web/src/common/hooks/useMediaQuery.ts @@ -0,0 +1,26 @@ +'use client'; + +import { useCallback, useSyncExternalStore } from 'react'; + +/** + * Reports whether a media query currently matches. + **/ +// An external store, not state in an effect: the server snapshot is `false` and the client is right on first render. +export function useMediaQuery(query: string): boolean { + const subscribe = useCallback( + (onChange: () => void) => { + const media = window.matchMedia(query); + + media.addEventListener('change', onChange); + + return () => media.removeEventListener('change', onChange); + }, + [query], + ); + + return useSyncExternalStore( + subscribe, + () => window.matchMedia(query).matches, + () => false, + ); +} diff --git a/apps/web/src/common/hooks/useSplitPanes.ts b/apps/web/src/common/hooks/useSplitPanes.ts new file mode 100644 index 0000000..7783ac7 --- /dev/null +++ b/apps/web/src/common/hooks/useSplitPanes.ts @@ -0,0 +1,188 @@ +'use client'; + +import type { StorageBackend } from '@dropto/types'; +import { useCallback, useEffect, useState } from 'react'; + +import { moveItemAction } from '@/actions/storage/storage.actions'; +import type { BrowsePane } from '@/common/hooks/useBrowsePane'; +import type { Crumb } from '@/common/types/workspace.types'; +import { useToast } from '@/components/providers/ToastProvider'; + +/** Which pane an interaction belongs to. */ +type Pane = 0 | 1; + +/** A drag in flight: what is being moved, and which pane it left. */ +interface DragMove { + ids: string[]; + sourcePane: Pane; +} + +interface Options { + /** False below the breakpoint where two panes fit. */ + canSplit: boolean; + backend: StorageBackend | null; + /** The main pane's folder and path. */ + currentFolderId: string | null; + path: Crumb[]; + paneB: BrowsePane; + reloadPanes: () => Promise; + /** Clears what the move invalidated (both selections and the preview), as the workspace sees fit. */ + onMoved: (ids: string[]) => void; +} + +/** + * The second pane: whether it is open, which pane is active, and moving items between them. + **/ +export function useSplitPanes({ + canSplit, + backend, + currentFolderId, + path, + paneB, + reloadPanes, + onMoved, +}: Options) { + const toast = useToast(); + + const [split, setSplit] = useState(false); + const [activePane, setActivePane] = useState(0); + const [dragMove, setDragMove] = useState(null); + + const toggleSplit = useCallback(() => { + setSplit((current) => { + const next = !current; + + if (next) { + paneB.goTo(path); + } else { + setDragMove(null); + setActivePane(0); + } + + return next; + }); + }, [paneB, path]); + + // Close the split (and any drag) if the storage disconnects entirely. + useEffect(() => { + if (backend === null) { + setSplit(false); + setDragMove(null); + } + }, [backend]); + + // Two panes have no usable width below `md`, and moving between them is a gesture touch never fires. + useEffect(() => { + if (!canSplit) { + setSplit(false); + setDragMove(null); + setActivePane(0); + } + }, [canSplit]); + + // Keyboard shortcut (Cmd/Ctrl + \) to toggle the split view. + useEffect(() => { + const handler = (event: KeyboardEvent) => { + if ((event.metaKey || event.ctrlKey) && event.key === '\\') { + const target = event.target as HTMLElement | null; + + if (target && (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA')) { + return; + } + // Only open the split from inside a folder, and only where it fits; closing is always allowed. + if (!split && (currentFolderId === null || !canSplit)) { + return; + } + + event.preventDefault(); + toggleSplit(); + } + }; + + window.addEventListener('keydown', handler); + + return () => window.removeEventListener('keydown', handler); + }, [split, currentFolderId, canSplit, toggleSplit]); + + /** + * Moves ids into a folder and reports the outcome in one live toast. + **/ + // A move can take a while (S3 copies whole folders), so the toast is opened before the requests. + const runMove = useCallback( + async (targetFolderId: string, ids: string[], activeBackend: StorageBackend) => { + const count = `${ids.length} item${ids.length === 1 ? '' : 's'}`; + const toastId = toast.loading(`Moving ${count}…`); + + const results = await Promise.all( + ids.map((id) => moveItemAction(activeBackend, id, targetFolderId)), + ); + const failed = results.filter((result) => !result.ok).length; + + await reloadPanes(); + onMoved(ids); + + toast.update(toastId, { + variant: failed > 0 ? 'error' : 'success', + message: + failed > 0 + ? `Failed to move ${failed} item${failed === 1 ? '' : 's'}.` + : `Moved ${count}.`, + }); + }, + [toast, reloadPanes, onMoved], + ); + + // Finder-style drop onto a folder row, within one pane. + const moveIntoFolder = useCallback( + async (targetFolderId: string, ids: string[]) => { + if (backend === null) { + return; + } + + const moveIds = ids.filter((id) => id !== targetFolderId); + + if (moveIds.length === 0) { + return; + } + + await runMove(targetFolderId, moveIds, backend); + }, + [backend, runMove], + ); + + // Drop onto the other pane. + const moveDrop = useCallback( + async (targetPane: Pane) => { + if (!dragMove || backend === null || dragMove.sourcePane === targetPane) { + return; + } + + const { ids } = dragMove; + const sourceFolderId = dragMove.sourcePane === 0 ? currentFolderId : paneB.currentFolderId; + const targetFolderId = targetPane === 0 ? currentFolderId : paneB.currentFolderId; + setDragMove(null); + + if (targetFolderId === null) { + return; + } + if (sourceFolderId === targetFolderId) { + toast.error('Items are already in that folder.'); + return; + } + + await runMove(targetFolderId, ids, backend); + }, + [dragMove, backend, currentFolderId, paneB, runMove, toast], + ); + + return { + split, + activePane, + setActivePane, + dragMove, + setDragMove, + toggleSplit, + moveIntoFolder, + moveDrop, + }; +} diff --git a/apps/web/src/common/hooks/useUploadQueue.ts b/apps/web/src/common/hooks/useUploadQueue.ts new file mode 100644 index 0000000..b590f7b --- /dev/null +++ b/apps/web/src/common/hooks/useUploadQueue.ts @@ -0,0 +1,284 @@ +'use client'; + +import type { StorageBackend } from '@dropto/types'; +import { useCallback, useRef, useState } from 'react'; + +import { createFolderAction, deleteItemAction } from '@/actions/storage/storage.actions'; +import { uploadFile } from '@/common/services/api/storage.client'; +import type { + BatchRuntime, + UploadBatch, + UploadItem, + ViewEntry, +} from '@/common/types/workspace.types'; +import { extractApiErrorMessage, isCanceledError } from '@/common/utils/error.functions'; +import { + isIgnoredUploadName, + isTopLevelFolder, + topLevelName, + uniqueName, +} from '@/common/utils/upload.functions'; +import { useToast } from '@/components/providers/ToastProvider'; +import { useUploadActions } from '@/components/providers/UploadProvider'; + +/** Where an upload is headed: the target folder and what is already in it. */ +export interface UploadTarget { + folderId: string | null; + entries: ViewEntry[]; +} + +/** The duplicate prompt plus the entry point for a drop. */ +export interface UploadQueue { + /** Top-level names that already exist in the target folder; null when nothing is being asked. */ + duplicate: string[] | null; + resolveDuplicate: (choice: 'replace' | 'keep' | 'cancel') => void; + handleUpload: (items: UploadItem[], pane?: 0 | 1) => Promise; +} + +/** + * Runs the upload queue: duplicate handling, folder creation, per-file progress and rollback. + **/ +// The panes are reached through `getPaneTarget`, so this never has to know how the workspace stores them. +export function useUploadQueue( + activeBackend: StorageBackend | null, + getPaneTarget: (pane: 0 | 1) => UploadTarget, + reloadPanes: () => Promise, +): UploadQueue { + const toast = useToast(); + const { setBatches, updateTask, setBatchStatus, scheduleRemoveBatch, batchRuntime } = + useUploadActions(); + + const [duplicate, setDuplicate] = useState(null); + const duplicateResolve = useRef<((choice: 'replace' | 'keep' | 'cancel') => void) | null>(null); + + // Undo a batch: delete everything it created so a cancel leaves nothing behind. + const rollbackBatch = useCallback( + async (batchId: string, backend: StorageBackend) => { + // Flip the batch and its rows to cancelled (the dock turns them red). + setBatches((current) => + current.map((batch) => + batch.id === batchId + ? { + ...batch, + status: 'canceling', + tasks: batch.tasks.map((task) => ({ ...task, status: 'canceled', rate: null })), + } + : batch, + ), + ); + const runtime = batchRuntime.current.get(batchId); + if (runtime) { + await Promise.all( + [...runtime.rootFolderIds, ...runtime.looseFileIds].map((id) => + deleteItemAction(backend, id), + ), + ); + batchRuntime.current.delete(batchId); + } + await reloadPanes(); + setBatchStatus(batchId, 'canceled'); + scheduleRemoveBatch(batchId); + }, + [reloadPanes, setBatchStatus, scheduleRemoveBatch, setBatches, batchRuntime], + ); + + const askDuplicate = useCallback((names: string[]): Promise<'replace' | 'keep' | 'cancel'> => { + setDuplicate(names); + return new Promise((resolve) => { + duplicateResolve.current = resolve; + }); + }, []); + + const resolveDuplicate = useCallback((choice: 'replace' | 'keep' | 'cancel') => { + setDuplicate(null); + duplicateResolve.current?.(choice); + duplicateResolve.current = null; + }, []); + + const handleUpload = useCallback( + async (droppedItems: UploadItem[], pane: 0 | 1 = 0) => { + const items = droppedItems.filter((item) => !isIgnoredUploadName(item.relativePath)); + const { folderId: targetFolderId, entries: existing } = getPaneTarget(pane); + if (targetFolderId === null || activeBackend === null || items.length === 0) { + return; + } + const backend = activeBackend; + + // Detect name conflicts among the top-level items being added here. + const existingNames = new Set(existing.map((entry) => entry.name)); + const topLevel = new Map(); + for (const item of items) { + const top = topLevelName(item.relativePath); + topLevel.set(top, topLevel.get(top) === true || isTopLevelFolder(item.relativePath)); + } + const conflicts = [...topLevel.keys()].filter((name) => existingNames.has(name)); + + const renameMap = new Map(); + // "Replace": upload first, delete originals only once they're safely in. + let replaceIds: string[] = []; + if (conflicts.length > 0) { + const choice = await askDuplicate(conflicts); + if (choice === 'cancel') { + return; + } + if (choice === 'replace') { + replaceIds = existing + .filter((entry) => conflicts.includes(entry.name)) + .map((entry) => entry.id); + } else { + const taken = new Set(existingNames); + for (const name of conflicts) { + const renamed = uniqueName(name, taken); + taken.add(renamed); + renameMap.set(name, renamed); + } + } + } + + // Apply any renames (folders via their path, loose files via an upload name). + const finalItems: UploadItem[] = items.map((item) => { + const top = topLevelName(item.relativePath); + const renamed = renameMap.get(top); + if (!renamed) { + return item; + } + return isTopLevelFolder(item.relativePath) + ? { ...item, relativePath: `${renamed}${item.relativePath.slice(top.length)}` } + : { ...item, relativePath: renamed, uploadName: renamed }; + }); + + const folderTops = new Set( + finalItems + .filter((item) => isTopLevelFolder(item.relativePath)) + .map((item) => topLevelName(item.relativePath)), + ); + const looseCount = finalItems.filter((item) => !isTopLevelFolder(item.relativePath)).length; + const kind: UploadBatch['kind'] = + folderTops.size === 1 && looseCount === 0 ? 'folder' : 'files'; + const folderName = kind === 'folder' ? [...folderTops][0] : null; + + const batchId = crypto.randomUUID(); + const runtime: BatchRuntime = { + controller: new AbortController(), + looseFileIds: [], + rootFolderIds: [], + }; + batchRuntime.current.set(batchId, runtime); + const { signal } = runtime.controller; + + const queue = finalItems.map((item) => { + const slash = item.relativePath.lastIndexOf('/'); + return { + taskId: crypto.randomUUID(), + item, + dirPath: slash === -1 ? '' : item.relativePath.slice(0, slash), + }; + }); + setBatches((current) => [ + ...current, + { + id: batchId, + kind, + folderName, + status: 'uploading', + tasks: queue.map(({ taskId, item }) => ({ + id: taskId, + name: item.uploadName ?? item.file.name, + size: item.file.size, + percent: 0, + rate: null, + status: 'pending' as const, + })), + }, + ]); + + // Recreate the nested folder structure, tracking top-level folders for cancel. + const folderIdByPath = new Map([['', targetFolderId]]); + const ensureFolder = async (dirPath: string): Promise => { + const cached = folderIdByPath.get(dirPath); + if (cached) { + return cached; + } + const slash = dirPath.lastIndexOf('/'); + const parentPath = slash === -1 ? '' : dirPath.slice(0, slash); + const name = slash === -1 ? dirPath : dirPath.slice(slash + 1); + const parentId = await ensureFolder(parentPath); + const result = await createFolderAction(backend, parentId, name); + if (!result.ok || !result.data) { + throw new Error(result.error ?? 'Failed to create folder.'); + } + const created = result.data; + folderIdByPath.set(dirPath, created.id); + if (parentPath === '') { + runtime.rootFolderIds.push(created.id); + } + return created.id; + }; + + for (const { taskId, item, dirPath } of queue) { + if (signal.aborted) { + break; + } + updateTask(batchId, taskId, { status: 'uploading' }); + try { + // eslint-disable-next-line no-await-in-loop -- uploads run one at a time (progress + folder dedup) + const uploadFolderId = await ensureFolder(dirPath); + // eslint-disable-next-line no-await-in-loop -- sequential upload queue + const result = await uploadFile( + backend, + uploadFolderId, + item.file, + ({ percent, rate }) => { + updateTask(batchId, taskId, { + percent, + rate, + status: percent >= 100 ? 'processing' : 'uploading', + }); + }, + signal, + item.uploadName, + ); + if (dirPath === '') { + runtime.looseFileIds.push(result.fileId); + } + updateTask(batchId, taskId, { status: 'done', percent: 100, rate: null }); + } catch (error) { + if (isCanceledError(error)) { + break; + } + updateTask(batchId, taskId, { status: 'error', rate: null }); + toast.error(`${item.uploadName ?? item.file.name}: ${extractApiErrorMessage(error)}`); + } + } + + if (signal.aborted) { + await rollbackBatch(batchId, backend); + } else { + try { + // Replacements are in — now remove the originals they superseded. + if (replaceIds.length > 0) { + await Promise.all(replaceIds.map((id) => deleteItemAction(backend, id))); + } + await reloadPanes(); + } finally { + batchRuntime.current.delete(batchId); + setBatchStatus(batchId, 'done'); + scheduleRemoveBatch(batchId); + } + } + }, + [ + activeBackend, + getPaneTarget, + reloadPanes, + toast, + askDuplicate, + updateTask, + setBatchStatus, + scheduleRemoveBatch, + rollbackBatch, + ], + ); + + return { duplicate, resolveDuplicate, handleUpload }; +} diff --git a/apps/web/src/common/hooks/useWorkspaceLocation.ts b/apps/web/src/common/hooks/useWorkspaceLocation.ts new file mode 100644 index 0000000..7a8875b --- /dev/null +++ b/apps/web/src/common/hooks/useWorkspaceLocation.ts @@ -0,0 +1,208 @@ +'use client'; + +import type { StorageBackend, StorageStatus } from '@dropto/types'; +import { usePathname, useRouter, useSearchParams } from 'next/navigation'; +import { useCallback, useEffect, useRef, useState } from 'react'; + +import { resolvePathAction } from '@/actions/storage/storage.actions'; +import type { Crumb, SortDir, SortKey, ViewEntry } from '@/common/types/workspace.types'; +import { buildWorkspaceUrl, slugify } from '@/common/utils/storage-url'; + +interface Options { + statuses: StorageStatus[]; + /** Backend from the URL (already validated connected), for first paint without a flash. */ + initialBackend?: StorageBackend | null; + /** Breadcrumb resolved server-side, so a deep link paints with real names. */ + initialPath?: Crumb[]; + /** The URL points at a folder that doesn't exist / isn't authorized (server-detected 404). */ + initialNotFound?: boolean; + /** Called whenever the location changes, so the workspace can drop the preview. */ + onLocationChange: () => void; +} + +/** + * First connected backend (or null) — used to auto-select a storage. + **/ +const pickDefaultBackend = (statuses: StorageStatus[]): StorageBackend | null => + statuses.find((status) => status.connected)?.backend ?? null; + +/** + * The browse location, derived from the URL: which backend, which folder path, and the sort. + **/ +// The URL is the source of truth, so the back button and a shared deep link behave identically. +export function useWorkspaceLocation({ + statuses, + initialBackend, + initialPath, + initialNotFound, + onLocationChange, +}: Options) { + const router = useRouter(); + const pathname = usePathname(); + const searchParams = useSearchParams(); + + const [activeBackend, setActiveBackend] = useState(initialBackend ?? null); + const [path, setPath] = useState(initialPath ?? []); + + // id → display name, so navigating (and rebuilding a deep-linked breadcrumb) shows real names. + const nameCache = useRef>( + new Map((initialPath ?? []).map((crumb) => [crumb.id, crumb.name])), + ); + // id → Drive web-view link, so the breadcrumb / toolbar "Copy link" always points at the real folder. + const linkCache = useRef>( + new Map((initialPath ?? []).map((crumb) => [crumb.id, crumb.webViewLink])), + ); + + // Server-detected 404 (bad folder URL): shown in the file area until the operator navigates away. + const notFoundPathname = useRef(initialNotFound ? pathname : null); + const notFound = notFoundPathname.current !== null && notFoundPathname.current === pathname; + + const currentFolderId = path.length > 0 ? path[path.length - 1].id : null; + const atRoots = path.length === 0; + + // Sort lives in the URL query (?sort=&dir=) so it's shareable; the main pane is controlled by it. + const sortParam = searchParams.get('sort'); + const sortKey: SortKey = sortParam === 'modified' || sortParam === 'size' ? sortParam : 'name'; + const sortDir: SortDir = searchParams.get('dir') === 'desc' ? 'desc' : 'asc'; + + // Derive the browse location from the URL; navigation updates it via window.history (no server round-trip). + useEffect(() => { + const segments = pathname.split('/').filter(Boolean).map(decodeURIComponent); + const paramBackend = segments[0] === 'drive' || segments[0] === 's3' ? segments[0] : null; + const connected = statuses.filter((status) => status.connected).map((status) => status.backend); + + // Server-detected 404: keep the sidebar (select the backend) but don't load the bad folder. + if (notFound) { + if (paramBackend && connected.includes(paramBackend)) { + setActiveBackend(paramBackend); + } + setPath([]); + return; + } + + // Only connected backends are browsable; redirect away from an unknown/disconnected URL. + if (paramBackend === null || !connected.includes(paramBackend)) { + const fallback = pickDefaultBackend(statuses); + if (fallback === null) { + setActiveBackend(null); + setPath([]); + onLocationChange(); + return; + } + router.replace(`/${fallback}`); + return; + } + + const backend = paramBackend; + const backendRoots = statuses.find((status) => status.backend === backend)?.roots ?? []; + const folderSegs = segments.slice(1); + const root = + folderSegs.length > 0 && backendRoots.find((r) => slugify(r.name) === folderSegs[0]); + + if (!root) { + setActiveBackend(backend); + setPath([]); + onLocationChange(); + return; + } + + // Names/links come from the cache so the breadcrumb paints instantly; any gap is filled once below. + const restIds = folderSegs.slice(1); + setActiveBackend(backend); + setPath([ + { id: root.id, name: root.name, webViewLink: null }, + ...restIds.map((id) => ({ + id, + name: nameCache.current.get(id) ?? '', + webViewLink: linkCache.current.get(id) ?? null, + })), + ]); + onLocationChange(); + + const missing = restIds.filter( + (id) => !nameCache.current.has(id) || !linkCache.current.has(id), + ); + if (missing.length === 0) { + return; + } + + let cancelled = false; + void (async () => { + const result = await resolvePathAction(backend, missing); + if (cancelled || !result.ok) { + return; + } + for (const pair of result.data ?? []) { + nameCache.current.set(pair.id, pair.name); + linkCache.current.set(pair.id, pair.webViewLink); + } + setPath((current) => + current.map((crumb) => ({ + ...crumb, + name: nameCache.current.get(crumb.id) ?? crumb.name, + webViewLink: linkCache.current.get(crumb.id) ?? crumb.webViewLink, + })), + ); + })(); + return () => { + cancelled = true; + }; + }, [pathname, statuses, router]); + const openFolder = useCallback( + (entry: ViewEntry) => { + if (activeBackend === null) { + return; + } + nameCache.current.set(entry.id, entry.name); + linkCache.current.set(entry.id, entry.webViewLink); + const url = buildWorkspaceUrl(activeBackend, [ + ...path, + { id: entry.id, name: entry.name, webViewLink: entry.webViewLink }, + ]); + window.history.pushState(null, '', url); + }, + [activeBackend, path], + ); + + const navigate = useCallback( + (index: number) => { + if (activeBackend === null) { + return; + } + const url = buildWorkspaceUrl(activeBackend, index < 0 ? [] : path.slice(0, index + 1)); + window.history.pushState(null, '', url); + }, + [activeBackend, path], + ); + + const selectStorage = useCallback((backend: StorageBackend) => { + window.history.pushState(null, '', `/${backend}`); + }, []); + + const handleToggleSort = useCallback( + (key: SortKey) => { + const nextDir: SortDir = key === sortKey ? (sortDir === 'asc' ? 'desc' : 'asc') : 'asc'; + const base = activeBackend + ? buildWorkspaceUrl(activeBackend, path) + : window.location.pathname; + // Default (name/asc) stays out of the URL to keep it clean. + const query = key === 'name' && nextDir === 'asc' ? '' : `?sort=${key}&dir=${nextDir}`; + window.history.replaceState(null, '', `${base}${query}`); + }, + [sortKey, sortDir, activeBackend, path], + ); + + return { + activeBackend, + path, + currentFolderId, + atRoots, + notFound, + sortKey, + sortDir, + openFolder, + navigate, + selectStorage, + toggleSort: handleToggleSort, + }; +} diff --git a/apps/web/src/common/services/api/auth.client.ts b/apps/web/src/common/services/api/auth.client.ts index 806fd17..baa1bed 100644 --- a/apps/web/src/common/services/api/auth.client.ts +++ b/apps/web/src/common/services/api/auth.client.ts @@ -1,13 +1,4 @@ -import { getEnvString } from '@/common/utils/environments.functions'; - /** - * Absolute API URL that starts the Google OAuth consent flow (browser navigates to it). + * Same-origin URL that starts the Google OAuth consent flow (browser navigates to it). **/ -export const getGoogleAuthUrl = (): string => { - const base = getEnvString({ - key: 'NEXT_PUBLIC_API_URL', - fallback: 'http://localhost:4000/api/v1', - }); - - return `${base.replace(/\/$/, '')}/google-auth/google`; -}; +export const getGoogleAuthUrl = (): string => '/api/oauth/google'; diff --git a/apps/web/src/common/services/api/passthrough.server.ts b/apps/web/src/common/services/api/passthrough.server.ts index f1df91a..2c7398c 100644 --- a/apps/web/src/common/services/api/passthrough.server.ts +++ b/apps/web/src/common/services/api/passthrough.server.ts @@ -5,10 +5,16 @@ import { NextResponse } from 'next/server'; import { ACCESS_EXP_SKEW_MS, ACCESS_TOKEN_COOKIE, + OAUTH_STATE_COOKIE, REFRESH_TOKEN_COOKIE, } from '@/common/constants/auth.constants'; import { refreshSession } from '@/common/services/auth/refresh.server'; -import { applyAuthCookies, type ParsedSetCookie } from '@/common/services/auth/tokens.server'; +import { + applyAuthCookies, + type CookieWriter, + type ParsedSetCookie, +} from '@/common/services/auth/tokens.server'; +import { forwardedForHeader } from '@/common/utils/client-ip.functions'; import { isAccessTokenFresh } from '@/common/utils/jwt.functions'; import { appServerConfig } from '@/configs/app/app.server-config'; @@ -77,14 +83,17 @@ export const apiAuthHeaders = async ( result.set('cookie', cookieHeader); } - const clientIp = headersList.get('cf-connecting-ip') ?? headersList.get('x-forwarded-for'); - if (clientIp) { - result.set('x-forwarded-for', clientIp); + const forwardedFor = forwardedForHeader(headersList); + if (forwardedFor) { + result.set('x-forwarded-for', forwardedFor); } return result; }; +/** Lifetime of the re-emitted OAuth `state` cookie — mirrors the API's own 10 minutes. */ +const OAUTH_STATE_MAX_AGE_S = 10 * 60; + const DOWNLOAD_HEADERS = ['content-type', 'content-disposition', 'content-length', 'accept-ranges']; /** @@ -113,3 +122,107 @@ export const proxyDownload = async (path: string, signal: AbortSignal): Promise< return response; }; + +interface OAuthDestination { + url: URL; + cookies?: { name: string; value: string; options: Parameters[2] }[]; +} + +interface OAuthRedirectOptions { + cookieHeader: string | null; + isAllowedTarget?: (target: URL) => boolean; + rebaseOn?: string; + transformDestination?: (destination: URL) => OAuthDestination; + fallbackUrl: string; + unauthorizedUrl: string; + rotated?: ParsedSetCookie[]; +} + +/** + * Runs one leg of the Google OAuth handshake through this origin instead of the API's. + **/ +export const proxyOAuthLeg = async ( + path: string, + { + cookieHeader, + isAllowedTarget, + rebaseOn, + transformDestination, + fallbackUrl, + unauthorizedUrl, + rotated = [], + }: OAuthRedirectOptions, +): Promise => { + let apiResponse: Response; + + try { + apiResponse = await fetch(apiUrl(path), { + headers: await apiAuthHeaders(undefined, cookieHeader ?? undefined), + redirect: 'manual', + }); + } catch { + return NextResponse.redirect(`${fallbackUrl}?error=api_unreachable`); + } + + if (apiResponse.status === 401 || apiResponse.status === 403) { + return NextResponse.redirect(unauthorizedUrl); + } + + const location = apiResponse.headers.get('location'); + let target: URL | null = null; + + try { + target = location ? new URL(location) : null; + } catch { + target = null; + } + + if (!target) { + return NextResponse.redirect(`${fallbackUrl}?error=oauth_failed`); + } + + const destination = rebaseOn + ? new URL(`${target.pathname}${target.search}`, rebaseOn) + : isAllowedTarget?.(target) + ? target + : null; + + if (!destination) { + return NextResponse.redirect(`${fallbackUrl}?error=oauth_failed`); + } + + // Claimed cookies go on this response, like the state nonce below — the one place they are certain to land. + const claimed = transformDestination?.(destination); + const response = NextResponse.redirect((claimed?.url ?? destination).toString()); + + for (const { name, value, options } of claimed?.cookies ?? []) { + response.cookies.set(name, value, options); + } + + for (const raw of apiResponse.headers.getSetCookie()) { + const [pair, ...attrs] = raw.split(';'); + const eq = pair.indexOf('='); + if (eq === -1 || pair.slice(0, eq).trim() !== OAUTH_STATE_COOKIE) { + continue; + } + + const value = pair.slice(eq + 1).trim(); + const clearing = attrs.some((attr) => /^\s*max-age=0\s*$/i.test(attr)) || value === ''; + + if (clearing) { + response.cookies.delete(OAUTH_STATE_COOKIE); + } else { + response.cookies.set(OAUTH_STATE_COOKIE, value, { + httpOnly: true, + secure: appServerConfig.nodeEnv.isProduction, + sameSite: 'lax', + path: '/', + maxAge: OAUTH_STATE_MAX_AGE_S, + }); + } + } + + applyAuthCookies(response.cookies, rotated); + + return response; +}; diff --git a/apps/web/src/common/services/api/storage.api.ts b/apps/web/src/common/services/api/storage.api.ts index e2aac7f..0fec545 100644 --- a/apps/web/src/common/services/api/storage.api.ts +++ b/apps/web/src/common/services/api/storage.api.ts @@ -4,10 +4,10 @@ import type { DriveEntryPage, ListContentsQuery, ResumableUploadSession, + ResumableUploadStatus, StorageBackend, StorageStatus, UploadResult, - UploadStatus, } from '@dropto/types'; import { getHttp } from '@/common/services/axios/axios.instance'; @@ -119,9 +119,9 @@ export const getUploadStatus = async ( backend: StorageBackend, uploadUrl: string, size: number, -): Promise => { +): Promise => { const http = await getHttp(); - const { data } = await http.post(`/storage/${backend}/upload-status`, { + const { data } = await http.post(`/storage/${backend}/upload-status`, { uploadUrl, size, }); diff --git a/apps/web/src/common/services/api/storage.client.ts b/apps/web/src/common/services/api/storage.client.ts index f6509b7..611f26e 100644 --- a/apps/web/src/common/services/api/storage.client.ts +++ b/apps/web/src/common/services/api/storage.client.ts @@ -141,23 +141,29 @@ const uploadToDriveDirect = async ( try { // PUT the remaining bytes to the Google session URL (the URL is the capability — no auth header/cookies); progress offset by confirmed bytes. // eslint-disable-next-line no-await-in-loop -- deliberate: one (resumable) attempt per loop turn - const res = await axios.put<{ id?: string }>(uploadUrl, offset > 0 ? file.slice(offset) : file, { - signal: attempt.signal, - withCredentials: false, - headers: { - 'Content-Type': mimeType, - ...(offset > 0 ? { 'Content-Range': `bytes ${offset}-${size - 1}/${size}` } : {}), + const res = await axios.put<{ id?: string }>( + uploadUrl, + offset > 0 ? file.slice(offset) : file, + { + signal: attempt.signal, + withCredentials: false, + headers: { + 'Content-Type': mimeType, + ...(offset > 0 ? { 'Content-Range': `bytes ${offset}-${size - 1}/${size}` } : {}), + }, + validateStatus: (statusCode) => + (statusCode >= 200 && statusCode < 300) || statusCode === 308, + onUploadProgress: (event) => { + if (!onProgress) return; + const loaded = offset + event.loaded; + onProgress({ + percent: Math.min(100, Math.round((loaded / size) * 100)), + rate: + typeof event.rate === 'number' && Number.isFinite(event.rate) ? event.rate : null, + }); + }, }, - validateStatus: (statusCode) => (statusCode >= 200 && statusCode < 300) || statusCode === 308, - onUploadProgress: (event) => { - if (!onProgress) return; - const loaded = offset + event.loaded; - onProgress({ - percent: Math.min(100, Math.round((loaded / size) * 100)), - rate: typeof event.rate === 'number' && Number.isFinite(event.rate) ? event.rate : null, - }); - }, - }); + ); if (res.status === 308) { recheck = true; // Google wants more — re-query the confirmed offset and continue continue; diff --git a/apps/web/src/common/services/auth/tokens.server.ts b/apps/web/src/common/services/auth/tokens.server.ts index a89bed6..4552440 100644 --- a/apps/web/src/common/services/auth/tokens.server.ts +++ b/apps/web/src/common/services/auth/tokens.server.ts @@ -2,7 +2,6 @@ import 'server-only'; import type { ResponseCookies } from 'next/dist/server/web/spec-extension/cookies'; import { ACCESS_TOKEN_COOKIE, REFRESH_TOKEN_COOKIE } from '@/common/constants/auth.constants'; -import { appServerConfig } from '@/configs/app/app.server-config'; /** Both `cookies()` and `NextResponse.cookies` satisfy this shape. */ export interface CookieWriter { @@ -10,16 +9,74 @@ export interface CookieWriter { delete: (name: string) => void; } +/** An auth cookie exactly as the API issued it — every attribute travels with it. */ export interface ParsedSetCookie { name: string; value: string; + domain?: string; + path?: string; maxAge?: number; + expires?: Date; + httpOnly?: boolean; + secure?: boolean; + sameSite?: 'lax' | 'strict' | 'none'; } const AUTH_COOKIE_NAMES = new Set([ACCESS_TOKEN_COOKIE, REFRESH_TOKEN_COOKIE]); /** - * Parses `Set-Cookie` headers, keeping only the auth cookies. + * Reads one `Set-Cookie` attribute list into the options a cookie writer takes. + **/ +const parseAttributes = (attrs: string[]): Omit => { + const parsed: Omit = {}; + + for (const attr of attrs) { + const [rawKey, ...rest] = attr.split('='); + const key = rawKey.trim().toLowerCase(); + const value = rest.join('=').trim(); + + switch (key) { + case 'domain': + parsed.domain = value || undefined; + break; + case 'path': + parsed.path = value; + break; + case 'max-age': { + const maxAge = Number(value); + if (Number.isFinite(maxAge)) { + parsed.maxAge = maxAge; + } + break; + } + case 'expires': { + const expires = new Date(value); + if (!Number.isNaN(expires.getTime())) { + parsed.expires = expires; + } + break; + } + case 'httponly': + parsed.httpOnly = true; + break; + case 'secure': + parsed.secure = true; + break; + case 'samesite': { + const sameSite = value.toLowerCase(); + if (sameSite === 'lax' || sameSite === 'strict' || sameSite === 'none') { + parsed.sameSite = sameSite; + } + break; + } + } + } + + return parsed; +}; + +/** + * Parses `Set-Cookie` headers, keeping the auth cookies with every attribute intact. **/ export const parseAuthSetCookies = (setCookies: string[] | undefined): ParsedSetCookie[] => { if (!setCookies) { @@ -40,49 +97,35 @@ export const parseAuthSetCookies = (setCookies: string[] | undefined): ParsedSet continue; } - const value = namePart.slice(eq + 1).trim(); - const maxAgeAttr = attrs - .map((attr) => attr.trim()) - .find((attr) => attr.toLowerCase().startsWith('max-age=')); - const maxAge = maxAgeAttr ? Number(maxAgeAttr.split('=')[1]) : undefined; - - parsed.push({ name, value, maxAge: Number.isFinite(maxAge) ? maxAge : undefined }); + parsed.push({ + name, + value: namePart.slice(eq + 1).trim(), + ...parseAttributes(attrs), + }); } return parsed; }; /** - * Writes auth cookies (httpOnly, secure + domain in production). + * Writes auth cookies onto this response, exactly as the API issued them. **/ export const applyAuthCookies = (writer: CookieWriter, cookiesToSet: ParsedSetCookie[]): void => { - const { isProduction } = appServerConfig.nodeEnv; - - for (const { name, value, maxAge } of cookiesToSet) { - writer.set(name, value, { - httpOnly: true, - secure: isProduction, - sameSite: 'lax', - path: '/', - domain: isProduction ? appServerConfig.cookieDomain : undefined, - maxAge, - }); + for (const { name, value, ...attributes } of cookiesToSet) { + writer.set(name, value, attributes); } }; /** - * Clears every auth cookie (logout / failed refresh). + * Clears every auth cookie when the API gave us no `Set-Cookie` to forward. **/ export const clearAuthCookies = (writer: CookieWriter): void => { - const { isProduction } = appServerConfig.nodeEnv; - for (const name of AUTH_COOKIE_NAMES) { writer.set(name, '', { httpOnly: true, - secure: isProduction, + secure: process.env.NODE_ENV === 'production', sameSite: 'lax', path: '/', - domain: isProduction ? appServerConfig.cookieDomain : undefined, maxAge: 0, }); } diff --git a/apps/web/src/common/services/axios/axios.instance.ts b/apps/web/src/common/services/axios/axios.instance.ts index ef83584..2e0d94d 100644 --- a/apps/web/src/common/services/axios/axios.instance.ts +++ b/apps/web/src/common/services/axios/axios.instance.ts @@ -8,6 +8,7 @@ import { REFRESH_TOKEN_COOKIE, } from '@/common/constants/auth.constants'; import { ApiUnavailableError } from '@/common/services/axios/axios.errors'; +import { forwardedForHeader } from '@/common/utils/client-ip.functions'; import { appServerConfig } from '@/configs/app/app.server-config'; // Server-only axios for the API, scoped to the request's cookies; tokens never reach the browser. @@ -37,9 +38,9 @@ export const getHttp = async (): Promise => { } // Forward the real client IP so the API's IP-keyed rate limiter isn't blind. - const clientIp = headersList.get('cf-connecting-ip') ?? headersList.get('x-forwarded-for'); - if (clientIp) { - config.headers.set('X-Forwarded-For', clientIp); + const forwardedFor = forwardedForHeader(headersList); + if (forwardedFor) { + config.headers.set('X-Forwarded-For', forwardedFor); } // Drop JSON content-type for multipart so the boundary is kept. diff --git a/apps/web/src/common/services/picker/usePicker.ts b/apps/web/src/common/services/picker/usePicker.ts index 000bb3a..3faccc9 100644 --- a/apps/web/src/common/services/picker/usePicker.ts +++ b/apps/web/src/common/services/picker/usePicker.ts @@ -11,14 +11,54 @@ export interface PickedFolder { name: string; } -// gapi/Picker globals have no bundled types. +/** A document handed back by the Picker (only the fields this hook reads). */ +interface PickerDocument { + id: string; + name?: string; +} + +/** Payload of the Picker's callback. */ +interface PickerCallbackData { + action: string; + docs?: PickerDocument[]; +} + +/** Chainable Picker view builder. */ +interface PickerView { + setSelectFolderEnabled: (enabled: boolean) => PickerView; + setIncludeFolders: (include: boolean) => PickerView; + setOwnedByMe: (ownedByMe: boolean) => PickerView; + setMimeTypes: (mimeTypes: string) => PickerView; +} + +/** Chainable Picker builder. */ +interface PickerBuilder { + addView: (view: PickerView) => PickerBuilder; + enableFeature: (feature: string) => PickerBuilder; + setOAuthToken: (token: string) => PickerBuilder; + setDeveloperKey: (apiKey: string) => PickerBuilder; + setAppId: (appId: string) => PickerBuilder; + setCallback: (callback: (data: PickerCallbackData) => void) => PickerBuilder; + build: () => { setVisible: (visible: boolean) => void }; +} + +/** The slice of the `google.picker` namespace this hook drives. */ +interface PickerApi { + DocsView: new (viewId: string) => PickerView; + PickerBuilder: new () => PickerBuilder; + ViewId: { FOLDERS: string }; + Feature: { MULTISELECT_ENABLED: string }; + Action: { PICKED: string }; +} + +// gapi/Picker ship no bundled types, so the globals are declared structurally. declare global { interface Window { gapi?: { load: (name: string, callback: () => void) => void; }; google?: { - picker: any; + picker: PickerApi; }; } } @@ -99,14 +139,14 @@ export function usePicker() { .enableFeature(picker.Feature.MULTISELECT_ENABLED) .setOAuthToken(token) .setDeveloperKey(apiKey) - .setCallback((data: any) => { + .setCallback((data) => { if (data.action !== picker.Action.PICKED) { return; } - const folders: PickedFolder[] = (data.docs ?? []).map((doc: any) => ({ - folderId: doc.id as string, - name: (doc.name as string) ?? 'Untitled', + const folders: PickedFolder[] = (data.docs ?? []).map((doc) => ({ + folderId: doc.id, + name: doc.name ?? 'Untitled', })); onPicked(folders); diff --git a/apps/web/src/common/utils/client-ip.functions.ts b/apps/web/src/common/utils/client-ip.functions.ts new file mode 100644 index 0000000..4711399 --- /dev/null +++ b/apps/web/src/common/utils/client-ip.functions.ts @@ -0,0 +1,6 @@ +/** + * The `X-Forwarded-For` chain to hand the API, or null when there is nothing trustworthy to forward. + **/ +// Only `x-forwarded-for`, verbatim: the rate-limit key depends on `trust proxy` counting back to our proxy's entry. +export const forwardedForHeader = (headersList: Headers): string | null => + headersList.get('x-forwarded-for'); diff --git a/apps/web/src/common/utils/drop-items.functions.ts b/apps/web/src/common/utils/drop-items.functions.ts new file mode 100644 index 0000000..3cb180a --- /dev/null +++ b/apps/web/src/common/utils/drop-items.functions.ts @@ -0,0 +1,71 @@ +import type { UploadItem } from '@/common/types/workspace.types'; + +/** + * Reads all entries from a directory reader (≤100 per call). + **/ +const readAllEntries = (reader: FileSystemDirectoryReader): Promise => + new Promise((resolve, reject) => reader.readEntries(resolve, reject)); + +/** + * Resolves a file entry into a File. + **/ +const entryToFile = (entry: FileSystemFileEntry): Promise => + new Promise((resolve, reject) => entry.file(resolve, reject)); + +/** + * Recursively walks a dropped entry into flat upload items. + **/ +const walkEntry = async ( + entry: FileSystemEntry, + prefix: string, + out: UploadItem[], +): Promise => { + if (entry.isFile) { + const file = await entryToFile(entry as FileSystemFileEntry); + out.push({ file, relativePath: `${prefix}${entry.name}` }); + return; + } + + const reader = (entry as FileSystemDirectoryEntry).createReader(); + let batch = await readAllEntries(reader); + while (batch.length > 0) { + for (const child of batch) { + // eslint-disable-next-line no-await-in-loop -- sequential walk into a shared, ordered list + await walkEntry(child, `${prefix}${entry.name}/`, out); + } + // eslint-disable-next-line no-await-in-loop -- the directory reader is stateful; read in sequence + batch = await readAllEntries(reader); + } +}; + +/** + * Dropped items → flat upload items (falls back to the plain file list). + **/ +export const resolveDropItems = async ( + entries: FileSystemEntry[], + flatFiles: File[], +): Promise => { + if (entries.length === 0) { + return flatFiles.map((file) => ({ file, relativePath: file.name })); + } + + const out: UploadItem[] = []; + for (const entry of entries) { + // eslint-disable-next-line no-await-in-loop -- entries walked sequentially into an ordered list + await walkEntry(entry, '', out); + } + return out; +}; + +/** + * Picks the icon name for a file's MIME type. + **/ +export const fileIcon = (mimeType: string | null): string => { + if (!mimeType) return 'Document'; + if (mimeType.startsWith('image/')) return 'Photo'; + if (mimeType.startsWith('video/')) return 'Film'; + if (mimeType.startsWith('audio/')) return 'MusicalNote'; + if (mimeType === 'application/pdf') return 'DocumentText'; + if (mimeType.includes('zip') || mimeType.includes('compressed')) return 'ArchiveBox'; + return 'Document'; +}; diff --git a/apps/web/src/common/utils/environments.functions.ts b/apps/web/src/common/utils/environments.functions.ts index 619eb49..41f6b1c 100644 --- a/apps/web/src/common/utils/environments.functions.ts +++ b/apps/web/src/common/utils/environments.functions.ts @@ -1,11 +1,9 @@ -// NEXT_PUBLIC_* vars are inlined at build time, so they must be referenced by full name. interface EnvOptions { key: string; fallback?: string; } const PUBLIC_ENV: Record = { - NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL, NEXT_PUBLIC_GOOGLE_API_KEY: process.env.NEXT_PUBLIC_GOOGLE_API_KEY, NEXT_PUBLIC_GOOGLE_APP_ID: process.env.NEXT_PUBLIC_GOOGLE_APP_ID, }; diff --git a/apps/web/src/common/utils/jwt.functions.ts b/apps/web/src/common/utils/jwt.functions.ts index 13682c0..69d4fd6 100644 --- a/apps/web/src/common/utils/jwt.functions.ts +++ b/apps/web/src/common/utils/jwt.functions.ts @@ -8,7 +8,7 @@ interface JwtPayload { /** * Decodes a JWT payload WITHOUT verifying its signature (proxy reads `exp` only). **/ -export const decodeJwtPayload = (token: string | undefined): JwtPayload | null => { +const decodeJwtPayload = (token: string | undefined): JwtPayload | null => { if (!token) { return null; } diff --git a/apps/web/src/components/common/Icon.tsx b/apps/web/src/components/common/Icon.tsx index 0a77f48..2ea4130 100644 --- a/apps/web/src/components/common/Icon.tsx +++ b/apps/web/src/components/common/Icon.tsx @@ -2,6 +2,7 @@ import { Archive, + ArrowLeftRight, BadgeCheck, Check, ChevronDown, @@ -58,6 +59,7 @@ const ICONS: Record = { ArrowTopRightOnSquare: ExternalLink, ArrowUpTray: Upload, ArrowsPointingIn: Move, + ArrowsRightLeft: ArrowLeftRight, Check: Check, CheckBadge: BadgeCheck, ChevronDown, diff --git a/apps/web/src/components/common/Modal.tsx b/apps/web/src/components/common/Modal.tsx index 75204b7..10bb833 100644 --- a/apps/web/src/components/common/Modal.tsx +++ b/apps/web/src/components/common/Modal.tsx @@ -1,7 +1,7 @@ 'use client'; -import { AnimatePresence, motion } from 'framer-motion'; -import { useEffect } from 'react'; +import { AnimatePresence, motion } from 'motion/react'; +import { useEffect, useId } from 'react'; import type { ExtendedProps } from '@/common/types/global.types'; import Icon from '@/components/common/Icon'; @@ -17,6 +17,8 @@ interface Props extends ExtendedProps { * Animated modal dialog with a backdrop and optional title. **/ export default function Modal({ open, onClose, title, maxWidth = 'max-w-md', children }: Props) { + const titleId = useId(); + useEffect(() => { if (!open) { return; @@ -52,12 +54,19 @@ export default function Modal({ open, onClose, title, maxWidth = 'max-w-md', chi exit={{ opacity: 0, scale: 0.96, y: 8 }} transition={{ type: 'spring', stiffness: 320, damping: 26 }} onClick={(event) => event.stopPropagation()} + role='dialog' + aria-modal='true' + aria-label={title ? undefined : 'Dialog'} + aria-labelledby={title ? titleId : undefined} className={`w-full border-zinc-300 bg-zinc-50 dark:border-zinc-700 dark:bg-zinc-800 ${maxWidth} max-h-[90vh] overflow-y-auto rounded-xl border shadow-2xl`}> {title && (
-

{title}

+

+ {title} +

diff --git a/apps/web/src/components/layouts/AccountSidebar.tsx b/apps/web/src/components/layouts/AccountSidebar.tsx index 5740c1f..084e21d 100644 --- a/apps/web/src/components/layouts/AccountSidebar.tsx +++ b/apps/web/src/components/layouts/AccountSidebar.tsx @@ -1,343 +1,15 @@ 'use client'; -import type { StorageBackend, StorageStatus } from '@dropto/types'; -import { AnimatePresence, motion } from 'framer-motion'; -import Image from 'next/image'; -import { type ReactNode, useEffect, useState } from 'react'; - -import { getGoogleAuthUrl } from '@/common/services/api/auth.client'; -import { formatBytes } from '@/common/utils/format.functions'; -import Button from '@/components/common/Button'; -import Icon from '@/components/common/Icon'; -import ThemeToggle from '@/components/common/ThemeToggle'; -import LoadingIndicator from '@/components/loadings/LoadingIndicator'; -import { STORAGE_ICON } from '@/configs/storage.config'; - -interface Props { - statuses: StorageStatus[]; - driveStatus: StorageStatus | null; - activeBackend: StorageBackend | null; - loading: boolean; - username: string; - saving: boolean; - isOwner: boolean; - onSelectStorage: (backend: StorageBackend) => void; - onManageFolders: () => void; - onDisconnect: () => void; - onLogout: () => void; - loggingOut: boolean; -} - -function SectionLabel({ children }: { children: ReactNode }) { - return ( -

- {children} -

- ); -} - -/** - * A storage-usage bar (used of total), colored green → amber (≥ 75%) → red (> 95%). - **/ -function StorageMeter({ usage, limit }: { usage: number; limit: number }) { - const pct = limit > 0 ? Math.min(100, Math.round((usage / limit) * 100)) : 0; - const color = pct > 95 ? 'bg-red-500' : pct >= 75 ? 'bg-amber-500' : 'bg-green-600'; - - return ( -
-
-
-
-

- {formatBytes(usage, 2)} of {formatBytes(limit, 2)} used -

-
- ); -} - -/** Directions (in px) the sparks fly out to on a click. */ -const SPARKS = [ - { x: -14, y: -22 }, - { x: -6, y: -34 }, - { x: 14, y: -34 }, - { x: 30, y: -20 }, - { x: 36, y: 2 }, - { x: -12, y: 14 }, -]; - -/** - * Easter egg: the session avatar. Clicking it shouts an ever-longer "Pikachuuuu!" and throws a few sparks around. - **/ -function PikachuAvatar({ username }: { username: string }) { - const [burst, setBurst] = useState<{ id: number; clicks: number } | null>(null); - - useEffect(() => { - if (!burst) return; - - const timeout = setTimeout(() => setBurst(null), 1400); - - return () => clearTimeout(timeout); - }, [burst]); - - return ( -
- - {burst && ( - - Pikach{'u'.repeat(Math.min(2 + burst.clicks, 12))}! - - )} - - - - {burst && - SPARKS.map((spark, index) => ( - - ⚡ - - ))} - - - - setBurst((prev) => ({ id: (prev?.id ?? 0) + 1, clicks: prev ? prev.clicks + 1 : 0 })) - } - whileHover={{ rotate: [0, -12, 12, -8, 8, 0], scale: 1.1 }} - whileTap={{ scale: 0.85 }} - animate={{ - boxShadow: burst ? '0 0 0 4px rgba(250,204,21,0.45)' : '0 0 0 0px rgba(250,204,21,0)', - }} - transition={{ duration: 0.6 }} - title='Pika pika!' - className='inline-flex h-8 w-8 cursor-pointer items-center justify-center overflow-hidden rounded-full bg-green-600/15'> - {username} - -
- ); -} +import AccountSidebarContent, { type AccountSidebarProps } from './AccountSidebarContent'; /** * Left pane: Google account + S3 status, storage switcher, and the session footer. **/ -export default function AccountSidebar({ - statuses, - driveStatus, - activeBackend, - loading, - username, - saving, - isOwner, - onSelectStorage, - onManageFolders, - onDisconnect, - onLogout, - loggingOut, -}: Props) { - const s3Status = statuses.find((status) => status.backend === 's3') ?? null; - const connectedStorages = statuses.filter((status) => status.connected); - +// Only the desktop shell — below `md` the same body is served by the swipe-open `MobileDrawer`. +export default function AccountSidebar(props: AccountSidebarProps) { return ( ); } diff --git a/apps/web/src/components/layouts/AccountSidebarContent.tsx b/apps/web/src/components/layouts/AccountSidebarContent.tsx new file mode 100644 index 0000000..1e3f1a4 --- /dev/null +++ b/apps/web/src/components/layouts/AccountSidebarContent.tsx @@ -0,0 +1,346 @@ +'use client'; + +import type { StorageBackend, StorageStatus } from '@dropto/types'; +import { AnimatePresence, motion } from 'motion/react'; +import Image from 'next/image'; +import { type ReactNode, useEffect, useState } from 'react'; + +import { getGoogleAuthUrl } from '@/common/services/api/auth.client'; +import { formatBytes } from '@/common/utils/format.functions'; +import Button from '@/components/common/Button'; +import Icon from '@/components/common/Icon'; +import ThemeToggle from '@/components/common/ThemeToggle'; +import LoadingIndicator from '@/components/loadings/LoadingIndicator'; +import { STORAGE_ICON } from '@/configs/storage.config'; + +export interface AccountSidebarProps { + /** Instance name, resolved on the server so the wordmark hydrates identically. */ + appName: string; + statuses: StorageStatus[]; + driveStatus: StorageStatus | null; + activeBackend: StorageBackend | null; + loading: boolean; + username: string; + saving: boolean; + isOwner: boolean; + onSelectStorage: (backend: StorageBackend) => void; + onManageFolders: () => void; + onDisconnect: () => void; + onLogout: () => void; + loggingOut: boolean; +} + +function SectionLabel({ children }: { children: ReactNode }) { + return ( +

+ {children} +

+ ); +} + +/** + * A storage-usage bar (used of total), colored green → amber (≥ 75%) → red (> 95%). + **/ +function StorageMeter({ usage, limit }: { usage: number; limit: number }) { + const pct = limit > 0 ? Math.min(100, Math.round((usage / limit) * 100)) : 0; + const color = pct > 95 ? 'bg-red-500' : pct >= 75 ? 'bg-amber-500' : 'bg-green-600'; + + return ( +
+
+
+
+

+ {formatBytes(usage, 2)} of {formatBytes(limit, 2)} used +

+
+ ); +} + +/** Directions (in px) the sparks fly out to on a click. */ +const SPARKS = [ + { x: -14, y: -22 }, + { x: -6, y: -34 }, + { x: 14, y: -34 }, + { x: 30, y: -20 }, + { x: 36, y: 2 }, + { x: -12, y: 14 }, +]; + +/** + * Easter egg: the session avatar. Clicking it shouts an ever-longer "Pikachuuuu!" and throws a few sparks around. + **/ +function PikachuAvatar({ username }: { username: string }) { + const [burst, setBurst] = useState<{ id: number; clicks: number } | null>(null); + + useEffect(() => { + if (!burst) return; + + const timeout = setTimeout(() => setBurst(null), 1400); + + return () => clearTimeout(timeout); + }, [burst]); + + return ( +
+ + {burst && ( + + Pikach{'u'.repeat(Math.min(2 + burst.clicks, 12))}! + + )} + + + + {burst && + SPARKS.map((spark, index) => ( + + ⚡ + + ))} + + + + setBurst((prev) => ({ id: (prev?.id ?? 0) + 1, clicks: prev ? prev.clicks + 1 : 0 })) + } + whileHover={{ rotate: [0, -12, 12, -8, 8, 0], scale: 1.1 }} + whileTap={{ scale: 0.85 }} + animate={{ + boxShadow: burst ? '0 0 0 4px rgba(250,204,21,0.45)' : '0 0 0 0px rgba(250,204,21,0)', + }} + transition={{ duration: 0.6 }} + title='Pika pika!' + className='inline-flex h-8 w-8 cursor-pointer items-center justify-center overflow-hidden rounded-full bg-green-600/15'> + {username} + +
+ ); +} + +/** + * The sidebar body — shared by the desktop rail (`AccountSidebar`) and the mobile drawer. + **/ +export default function AccountSidebarContent({ + appName, + statuses, + driveStatus, + activeBackend, + loading, + username, + saving, + isOwner, + onSelectStorage, + onManageFolders, + onDisconnect, + onLogout, + loggingOut, +}: AccountSidebarProps) { + const s3Status = statuses.find((status) => status.backend === 's3') ?? null; + const connectedStorages = statuses.filter((status) => status.connected); + + return ( + <> +
+
+ +
+
+

{appName}

+

+ Storage workspace +

+
+
+ +
+
+ Google account + + {loading ? ( +
+ + Loading… +
+ ) : !driveStatus?.connected ? ( +
+

+ {driveStatus?.error ?? + 'No account connected. Connect a Google account to pick folders and upload.'} +

+ + + {driveStatus?.error ? 'Reconnect Drive' : 'Connect Drive'} + +
+ ) : ( +
+
+
+ +
+
+

{driveStatus.email}

+

Connected

+
+
+ + {driveStatus.quota && driveStatus.quota.limit !== null && ( + + )} + + {isOwner ? ( +
+ + +
+ ) : ( + + + Verify with Google to manage + + )} +
+ )} +
+ +
+ S3 storage + + {loading ? ( +
+ + Loading… +
+ ) : !s3Status?.connected ? ( +
+

+ {s3Status?.error ? 'S3 storage unavailable' : 'No S3 storage connected'} +

+

+ {s3Status?.error ?? + 'Enable and configure S3 in the API environment to browse buckets.'} +

+
+ ) : ( +
+
+ +
+
+

+ {s3Status.roots.length} bucket{s3Status.roots.length === 1 ? '' : 's'} +

+

Connected

+
+
+ )} +
+ + {!loading && connectedStorages.length > 0 && ( +
+ Browse + +
+ )} +
+ +
+
+ + {username} +
+
+ + +
+
+ + ); +} diff --git a/apps/web/src/components/layouts/Breadcrumb.tsx b/apps/web/src/components/layouts/Breadcrumb.tsx index 11f1504..c26a53f 100644 --- a/apps/web/src/components/layouts/Breadcrumb.tsx +++ b/apps/web/src/components/layouts/Breadcrumb.tsx @@ -1,7 +1,7 @@ 'use client'; import type { StorageBackend, StorageStatus } from '@dropto/types'; -import { AnimatePresence, motion } from 'framer-motion'; +import { AnimatePresence, motion } from 'motion/react'; import { useEffect, useRef, useState } from 'react'; import { createPortal } from 'react-dom'; @@ -103,10 +103,7 @@ export default function Breadcrumb({ const renderCrumb = (crumb: Crumb, index: number) => ( - +
    - {sorted.map((entry) => { - const selected = entry.id === selectedId; - const checked = selectedIds.has(entry.id); - return ( -
  • -
    handleRowDragStart(event, entry)} - onDragEnd={() => onMoveDragEnd?.()} - onDragOver={(event) => { - // A folder row accepts a move drop (unless it's part of the dragged selection). - if ( - canModify && - onMoveIntoFolder && - entry.isFolder && - !selectedIds.has(entry.id) && - event.dataTransfer.types.includes(MOVE_MIME) - ) { - event.preventDefault(); - event.stopPropagation(); - event.dataTransfer.dropEffect = 'move'; - if (dropFolderId !== entry.id) setDropFolderId(entry.id); - } - }} - onDragLeave={(event) => { - if (event.currentTarget.contains(event.relatedTarget as Node | null)) { - return; - } - setDropFolderId((current) => (current === entry.id ? null : current)); - }} - onDrop={(event) => { - if ( - !canModify || - !onMoveIntoFolder || - !entry.isFolder || - !event.dataTransfer.types.includes(MOVE_MIME) - ) { - return; - } - event.preventDefault(); - event.stopPropagation(); - setDropFolderId(null); - const ids = event.dataTransfer.getData(MOVE_MIME).split(',').filter(Boolean); - if (ids.length > 0 && !ids.includes(entry.id)) { - onMoveIntoFolder(entry.id, ids); - } - }} - onClick={(event) => { - // Don't let the click reach the finder's deselect handler. - event.stopPropagation(); - const ids = sorted.map((row) => row.id); - // Shift-click: select the range from the anchor to this row. - if (event.shiftKey && onSetSelectedIds && selectionAnchor.current) { - const from = ids.indexOf(selectionAnchor.current); - const to = ids.indexOf(entry.id); - if (from !== -1 && to !== -1) { - const [lo, hi] = from < to ? [from, to] : [to, from]; - onSetSelectedIds(ids.slice(lo, hi + 1)); - return; - } - } - // Cmd/Ctrl-click: toggle this row in the multi-selection. - if ((event.metaKey || event.ctrlKey) && canModify) { - onToggleSelect(entry.id); - selectionAnchor.current = entry.id; - return; - } - // Plain click: single select (preview) and drop any multi-selection. - onSelect(entry); - if (canModify) { - onClearSelection(); - } - selectionAnchor.current = entry.id; - }} - onDoubleClick={() => { - if (entry.isFolder) onOpenFolder(entry); - else if (entry.webViewLink) window.open(entry.webViewLink, '_blank'); - }} - onKeyDown={(event) => { - // Enter opens a folder / selects a file; Space selects. - if (event.key === 'Enter') { - event.preventDefault(); - if (entry.isFolder) onOpenFolder(entry); - else onSelect(entry); - } else if (event.key === ' ') { - event.preventDefault(); - onSelect(entry); - } - }} - className={`grid ${gridCols} w-full cursor-pointer items-center gap-x-3 rounded-lg px-3 py-2 text-left transition ${ - dropFolderId === entry.id - ? 'bg-green-600/15 ring-2 ring-green-600 ring-inset' - : checked || selected - ? 'bg-green-600/10' - : 'hover:bg-zinc-200 dark:hover:bg-zinc-800' - }`}> - {canModify && ( - // Wrapper keeps the grid cell aligned on desktop; the checkbox shows only on mobile. - - event.stopPropagation()} - onChange={() => onToggleSelect(entry.id)} - className='hidden h-4 w-4 max-sm:block' - /> - {/* Desktop: a check indicator (no checkbox) for selected rows. */} - {checked && ( - - )} - - )} - - {entry.isFolder ? ( - - ) : ( - - )} - {entry.name} - - - {formatDate(entry.modifiedTime)} - - - {entry.isFolder ? '—' : formatBytes(entry.size)} - - {(canModify || (rootMenu && entry.isFolder)) && ( -
    - -
    - )} -
    -
  • - ); - })} + {sorted.map((entry) => ( + + ))}
{loadingMore && ( @@ -894,7 +827,7 @@ export default function FileBrowser({ onClick={() => runToolbarAction(onNewFolder)} /> )} - {onToggleSplit && ( + {onToggleSplit && canSplit && ( , entry: ViewEntry) => void; + onDragEnd: () => void; + onDragOver: (event: DragEvent, entry: ViewEntry) => void; + onDragLeave: (event: DragEvent, entry: ViewEntry) => void; + onDrop: (event: DragEvent, entry: ViewEntry) => void; + onClick: (event: MouseEvent, entry: ViewEntry) => void; + onDoubleClick: (entry: ViewEntry) => void; + onKeyDown: (event: KeyboardEvent, entry: ViewEntry) => void; + onToggleCheck: (entry: ViewEntry) => void; + onOpenMenu: (event: MouseEvent, entry: ViewEntry) => void; +} + +/** + * One row of the file list: icon, name, modified date, size and the action menu. + **/ +// Memoized: every prop is a scalar or a browser-stable callback, so selecting a row re-renders only that row. +function FileRow({ + entry, + selected, + checked, + isDropTarget, + menuOpen, + canModify, + showMenu, + gridCols, + onDragStart, + onDragEnd, + onDragOver, + onDragLeave, + onDrop, + onClick, + onDoubleClick, + onKeyDown, + onToggleCheck, + onOpenMenu, +}: Props) { + return ( +
  • +
    onDragStart(event, entry)} + onDragEnd={onDragEnd} + onDragOver={(event) => onDragOver(event, entry)} + onDragLeave={(event) => onDragLeave(event, entry)} + onDrop={(event) => onDrop(event, entry)} + onClick={(event) => onClick(event, entry)} + onDoubleClick={() => onDoubleClick(entry)} + onKeyDown={(event) => onKeyDown(event, entry)} + className={`grid ${gridCols} w-full cursor-pointer items-center gap-x-3 rounded-lg px-3 py-2 text-left transition ${ + isDropTarget + ? 'bg-green-600/15 ring-2 ring-green-600 ring-inset' + : checked || selected + ? 'bg-green-600/10' + : 'hover:bg-zinc-200 dark:hover:bg-zinc-800' + }`}> + {canModify && ( + // Wrapper keeps the grid cell aligned on desktop; the checkbox shows only on mobile. + + event.stopPropagation()} + onChange={() => onToggleCheck(entry)} + className='hidden h-4 w-4 max-sm:block' + /> + {/* Desktop: a check indicator (no checkbox) for selected rows. */} + {checked && } + + )} + + {entry.isFolder ? ( + + ) : ( + + )} + {entry.name} + + + {formatDate(entry.modifiedTime)} + + + {entry.isFolder ? '—' : formatBytes(entry.size)} + + {showMenu && ( +
    + +
    + )} +
    +
  • + ); +} + +export default memo(FileRow); diff --git a/apps/web/src/components/layouts/NotFoundContent.tsx b/apps/web/src/components/layouts/NotFoundContent.tsx index 9159be5..3ca27b8 100644 --- a/apps/web/src/components/layouts/NotFoundContent.tsx +++ b/apps/web/src/components/layouts/NotFoundContent.tsx @@ -1,32 +1,22 @@ -'use client'; - -import { motion } from 'framer-motion'; import Link from 'next/link'; -import Icon from '@/components/common/Icon'; - /** - * Animated 404 body (client, so the not-found route stays a Server Component). + * 404 body for the not-found route. **/ +// Deliberately static: an error page must not depend on JS having run to become visible. export default function NotFoundContent() { return ( -
    - - -

    Page not found

    -

    - The page you are looking for doesn't exist or has been moved. -

    - - Back to home - -
    +
    +

    Error 404

    +

    Page not found

    +

    + This page does not exist — but your storage is one click away. +

    + + Back to the workspace +
    ); } diff --git a/apps/web/src/components/layouts/PreviewPanel.tsx b/apps/web/src/components/layouts/PreviewPanel.tsx index aa10858..4298017 100644 --- a/apps/web/src/components/layouts/PreviewPanel.tsx +++ b/apps/web/src/components/layouts/PreviewPanel.tsx @@ -1,7 +1,7 @@ 'use client'; import type { StorageBackend } from '@dropto/types'; -import { AnimatePresence, motion } from 'framer-motion'; +import { AnimatePresence, motion } from 'motion/react'; import { useState } from 'react'; import { fileDownloadUrl } from '@/common/services/api/storage.client'; diff --git a/apps/web/src/components/layouts/UploadDock.tsx b/apps/web/src/components/layouts/UploadDock.tsx index a4b3c8d..d731d8d 100644 --- a/apps/web/src/components/layouts/UploadDock.tsx +++ b/apps/web/src/components/layouts/UploadDock.tsx @@ -1,6 +1,6 @@ 'use client'; -import { AnimatePresence, motion } from 'framer-motion'; +import { AnimatePresence, motion } from 'motion/react'; import { useState } from 'react'; import { useOnlineStatus } from '@/common/hooks/useOnlineStatus'; diff --git a/apps/web/src/components/layouts/WorkspaceClient.tsx b/apps/web/src/components/layouts/WorkspaceClient.tsx index 852ff5c..31a7623 100644 --- a/apps/web/src/components/layouts/WorkspaceClient.tsx +++ b/apps/web/src/components/layouts/WorkspaceClient.tsx @@ -2,68 +2,46 @@ import type { StorageBackend, StorageStatus } from '@dropto/types'; import { usePathname, useRouter, useSearchParams } from 'next/navigation'; -import { type FormEvent, useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { - claimDriveOwnerAction, disconnectAction, - removeFolderAction, revokeDriveOwnerAction, saveFoldersAction, } from '@/actions/auth/auth.actions'; -import { - createFolderAction, - deleteItemAction, - listContentsAction, - moveItemAction, - renameItemAction, - resolvePathAction, - statusesAction, -} from '@/actions/storage/storage.actions'; +import { statusesAction } from '@/actions/storage/storage.actions'; +import { DESKTOP_QUERY } from '@/common/constants/layout.constants'; import { useBrowsePane } from '@/common/hooks/useBrowsePane'; import { useDebouncedValue } from '@/common/hooks/useDebouncedValue'; -import { - fileDownloadUrl, - folderDownloadUrl, - uploadFile, -} from '@/common/services/api/storage.client'; +import { useEntryListing } from '@/common/hooks/useEntryListing'; +import { useEntryOperations } from '@/common/hooks/useEntryOperations'; +import { useEntrySelection } from '@/common/hooks/useEntrySelection'; +import { useMediaQuery } from '@/common/hooks/useMediaQuery'; +import { useSplitPanes } from '@/common/hooks/useSplitPanes'; +import { useUploadQueue } from '@/common/hooks/useUploadQueue'; +import { useWorkspaceLocation } from '@/common/hooks/useWorkspaceLocation'; +import { fileDownloadUrl, folderDownloadUrl } from '@/common/services/api/storage.client'; import { type PickedFolder, usePicker } from '@/common/services/picker/usePicker'; -import type { - BatchRuntime, - Crumb, - SortDir, - SortKey, - UploadBatch, - UploadItem, - ViewEntry, -} from '@/common/types/workspace.types'; -import { extractApiErrorMessage, isCanceledError } from '@/common/utils/error.functions'; -import { buildWorkspaceUrl, slugify } from '@/common/utils/storage-url'; -import { - isIgnoredUploadName, - isTopLevelFolder, - topLevelName, - uniqueName, -} from '@/common/utils/upload.functions'; -import { toViewEntries } from '@/common/utils/view-entry.functions'; -import Button from '@/components/common/Button'; -import ConfirmDialog from '@/components/common/ConfirmDialog'; -import Icon from '@/components/common/Icon'; -import Input from '@/components/common/Input'; -import Modal from '@/components/common/Modal'; +import type { Crumb, ViewEntry } from '@/common/types/workspace.types'; +import { extractApiErrorMessage } from '@/common/utils/error.functions'; import { useToast } from '@/components/providers/ToastProvider'; import { UploadProvider, useUploadActions } from '@/components/providers/UploadProvider'; import { STORAGE_ICON } from '@/configs/storage.config'; import AccountSidebar from './AccountSidebar'; +import type { AccountSidebarProps } from './AccountSidebarContent'; import FileBrowser from './FileBrowser'; +import MobileMenu from './mobile/MobileMenu'; import PreviewPanel from './PreviewPanel'; import UploadDock from './UploadDock'; +import WorkspaceDialogs from './WorkspaceDialogs'; const FOLDER_MIME = 'application/vnd.google-apps.folder'; const DOWNLOAD_PREPARING_MS = 6000; interface Props { + /** Instance name, resolved on the server so the wordmark hydrates identically. */ + appName: string; username: string; /** Statuses fetched server-side, for first paint. */ initialStatuses: StorageStatus[]; @@ -75,125 +53,61 @@ interface Props { initialNotFound?: boolean; } -/** - * First connected backend (or null) — used to auto-select a storage. - **/ -const pickDefaultBackend = (statuses: StorageStatus[]): StorageBackend | null => - statuses.find((status) => status.connected)?.backend ?? null; - -/** - * Splits a name into base + extension (leading/trailing dot = no extension). - **/ -const splitExtension = (name: string): { base: string; ext: string } => { - const dot = name.lastIndexOf('.'); - if (dot <= 0 || dot === name.length - 1) { - return { base: name, ext: '' }; - } - return { base: name.slice(0, dot), ext: name.slice(dot) }; -}; - -/** A pending extension change awaiting confirmation (the "modal on a modal"). */ -interface ExtensionWarning { - /** The name exactly as typed (keeps the new extension). */ - use: string; - /** The typed base name but with the original extension kept. */ - keep: string; - /** The original extension (`''` when the file had none). */ - fromExt: string; - /** The new extension (`''` when the new name has none). */ - toExt: string; -} - /** * Main workspace: sidebar + file browser + preview, with uploads and account management. **/ function WorkspaceInner({ + appName, username, initialStatuses, initialBackend, initialPath, initialNotFound, }: Props) { - const router = useRouter(); const searchParams = useSearchParams(); + const router = useRouter(); const pathname = usePathname(); const toast = useToast(); const { openPicker } = usePicker(); - const { - setBatches, - setDownloads, - updateTask, - setBatchStatus, - scheduleRemoveBatch, - batchRuntime, - } = useUploadActions(); + const { setDownloads } = useUploadActions(); const [statuses, setStatuses] = useState(initialStatuses); const [loadingStatus, setLoadingStatus] = useState(false); - const [activeBackend, setActiveBackend] = useState(initialBackend ?? null); - const [path, setPath] = useState(initialPath ?? []); - const [entries, setEntries] = useState([]); - const [loadingEntries, setLoadingEntries] = useState(false); const [search, setSearch] = useState(''); - const [nextPageToken, setNextPageToken] = useState(null); - const [loadingMore, setLoadingMore] = useState(false); const [selected, setSelected] = useState(null); - const [duplicate, setDuplicate] = useState(null); - const [confirmTarget, setConfirmTarget] = useState(null); - const [deleting, setDeleting] = useState(false); - const [unselectTarget, setUnselectTarget] = useState(null); - const [unselecting, setUnselecting] = useState(false); - const [selectedIds, setSelectedIds] = useState>(new Set()); - const [bulkDeleteOpen, setBulkDeleteOpen] = useState(false); - const [bulkDeleting, setBulkDeleting] = useState(false); - const [split, setSplit] = useState(false); - const [activePane, setActivePane] = useState<0 | 1>(0); - const [bulkPane, setBulkPane] = useState<0 | 1>(0); - const [dragMove, setDragMove] = useState<{ ids: string[]; sourcePane: 0 | 1 } | null>(null); - const [renameTarget, setRenameTarget] = useState(null); - const [renameName, setRenameName] = useState(''); - const [renaming, setRenaming] = useState(false); - const [pendingRename, setPendingRename] = useState(null); - const [extWarning, setExtWarning] = useState(null); - const [newFolderOpen, setNewFolderOpen] = useState(false); - const [newFolderName, setNewFolderName] = useState(''); - const [newFolderPane, setNewFolderPane] = useState<0 | 1>(0); - const [creating, setCreating] = useState(false); + const canSplit = useMediaQuery(DESKTOP_QUERY); const [saving, setSaving] = useState(false); const [loggingOut, setLoggingOut] = useState(false); const handledParams = useRef(false); - const duplicateResolve = useRef<((choice: 'replace' | 'keep' | 'cancel') => void) | null>(null); - // Bumped on every listing read; a resolved request whose id no longer matches is stale (the - // operator navigated away mid-flight) and must not paint over the current folder. - const listSeq = useRef(0); - // id → display name, so navigating (and rebuilding a deep-linked breadcrumb) shows real names. - const nameCache = useRef>( - new Map((initialPath ?? []).map((crumb) => [crumb.id, crumb.name])), - ); - // id → Drive web-view link, so the breadcrumb / toolbar "Copy link" always points at the real folder. - const linkCache = useRef>( - new Map((initialPath ?? []).map((crumb) => [crumb.id, crumb.webViewLink])), - ); + + const { + activeBackend, + path, + currentFolderId, + atRoots, + notFound, + sortKey, + sortDir, + openFolder, + navigate, + selectStorage, + toggleSort: handleToggleSort, + } = useWorkspaceLocation({ + statuses, + initialBackend, + initialPath, + initialNotFound, + onLocationChange: () => setSelected(null), + }); const driveStatus = statuses.find((status) => status.backend === 'drive') ?? null; const activeStatus = activeBackend ? (statuses.find((status) => status.backend === activeBackend) ?? null) : null; - const currentFolderId = path.length > 0 ? path[path.length - 1].id : null; - const atRoots = path.length === 0; const debouncedSearch = useDebouncedValue(search.trim(), 250); - // Server-detected 404 (bad folder URL): shown in the file area until the operator navigates away. - const notFoundPathname = useRef(initialNotFound ? pathname : null); - const notFound = notFoundPathname.current !== null && notFoundPathname.current === pathname; - - // Sort lives in the URL query (?sort=&dir=) so it's shareable; the main pane is controlled by it. - const sortParam = searchParams.get('sort'); - const sortKey: SortKey = sortParam === 'modified' || sortParam === 'size' ? sortParam : 'name'; - const sortDir: SortDir = searchParams.get('dir') === 'desc' ? 'desc' : 'asc'; - const roots = useMemo( () => (activeStatus?.roots ?? []).map((root) => ({ @@ -223,8 +137,7 @@ function WorkspaceInner({ const onPaneError = useCallback( (error: { error?: string; status?: number }) => { toast.error(error.error ?? 'Failed to open the folder.'); - // The storage went away mid-session (revoked Drive token / dead S3), or the folder itself was - // deleted outside the app — either way the roots in the sidebar may be stale. + // The storage or the folder went away mid-session, so the sidebar's roots may be stale. if (error.status === 424 || error.status === 404) { void loadStatus(); } @@ -232,95 +145,50 @@ function WorkspaceInner({ [toast, loadStatus], ); - // The second (split) pane: an independent browser over the same backend. - const paneB = useBrowsePane(activeBackend, roots, onPaneError); - - // Derive the browse location from the URL; navigation updates it via window.history (no server round-trip). - useEffect(() => { - const segments = pathname.split('/').filter(Boolean).map(decodeURIComponent); - const paramBackend = segments[0] === 'drive' || segments[0] === 's3' ? segments[0] : null; - const connected = statuses.filter((status) => status.connected).map((status) => status.backend); - - // Server-detected 404: keep the sidebar (select the backend) but don't load the bad folder. - if (notFound) { - if (paramBackend && connected.includes(paramBackend)) { - setActiveBackend(paramBackend); - } - setPath([]); - return; - } - - // Only connected backends are browsable; redirect away from an unknown/disconnected URL. - if (paramBackend === null || !connected.includes(paramBackend)) { - const fallback = pickDefaultBackend(statuses); - if (fallback === null) { - setActiveBackend(null); - setPath([]); - setSelected(null); - return; - } - router.replace(`/${fallback}`); - return; - } - - const backend = paramBackend; - const backendRoots = statuses.find((status) => status.backend === backend)?.roots ?? []; - const folderSegs = segments.slice(1); - const root = - folderSegs.length > 0 && backendRoots.find((r) => slugify(r.name) === folderSegs[0]); - - if (!root) { - setActiveBackend(backend); - setPath([]); - setSelected(null); - return; - } - - // Names/links come from the cache so the breadcrumb paints instantly; any gap is filled once below. - const restIds = folderSegs.slice(1); - setActiveBackend(backend); - setPath([ - { id: root.id, name: root.name, webViewLink: null }, - ...restIds.map((id) => ({ - id, - name: nameCache.current.get(id) ?? '', - webViewLink: linkCache.current.get(id) ?? null, - })), - ]); - setSelected(null); + // Same primitives the split pane runs on; only the location differs (URL here, local state there). + const { + entries, + loading: loadingEntries, + hasMore, + loadingMore, + loadMore: loadMoreEntries, + reload: loadEntries, + } = useEntryListing({ + backend: activeBackend, + currentFolderId, + roots, + search: debouncedSearch, + sortKey, + sortDir, + onError: onPaneError, + }); - const missing = restIds.filter((id) => !nameCache.current.has(id) || !linkCache.current.has(id)); - if (missing.length === 0) { - return; - } + const { + selectedIds, + toggleSelect, + selectAll: selectIds, + clearSelection, + setSelection, + pruneSelection, + } = useEntrySelection(currentFolderId, activeBackend); + + const selectAll = useCallback( + () => selectIds(entries.map((entry) => entry.id)), + [entries, selectIds], + ); - let cancelled = false; - void (async () => { - const result = await resolvePathAction(backend, missing); - if (cancelled || !result.ok) { - return; - } - for (const pair of result.data ?? []) { - nameCache.current.set(pair.id, pair.name); - linkCache.current.set(pair.id, pair.webViewLink); - } - setPath((current) => - current.map((crumb) => ({ - ...crumb, - name: nameCache.current.get(crumb.id) ?? crumb.name, - webViewLink: linkCache.current.get(crumb.id) ?? crumb.webViewLink, - })), - ); - })(); - return () => { - cancelled = true; - }; - }, [pathname, statuses, router]); + // The second (split) pane: an independent browser over the same backend. + const paneB = useBrowsePane(activeBackend, roots, onPaneError); - // Drop any multi-select whenever the folder or storage changes. - useEffect(() => { - setSelectedIds(new Set()); - }, [currentFolderId, activeBackend]); + // A move empties both selections and drops the preview if it was one of the moved items. + const handleMoved = useCallback( + (ids: string[]) => { + clearSelection(); + paneB.clearSelection(); + setSelected((current) => (current && ids.includes(current.id) ? null : current)); + }, + [clearSelection, paneB], + ); useEffect(() => { if (handledParams.current) { @@ -330,147 +198,29 @@ function WorkspaceInner({ if (searchParams.get('connected') === '1') { toast.success('Google account connected successfully.'); - const ownerToken = searchParams.get('ownerToken'); - void (async () => { - if (ownerToken) { - await claimDriveOwnerAction(ownerToken); - } - await loadStatus(); - })(); + void loadStatus(); } const error = searchParams.get('error'); if (error) { toast.error(`Failed to connect the Google account (${error}).`); } if (searchParams.get('connected') || searchParams.get('error')) { - window.history.replaceState(null, '', window.location.pathname); + // Through the router: a raw replaceState leaves the App Router's canonical URL stale and a Server Action restores it. + router.replace(pathname, { scroll: false }); } - }, [searchParams, toast, loadStatus]); + }, [searchParams, toast, loadStatus, router, pathname]); // A new folder starts unfiltered. useEffect(() => { setSearch(''); }, [currentFolderId, activeBackend]); - const loadEntries = useCallback(async () => { - const seq = ++listSeq.current; - - if (currentFolderId === null || activeBackend === null) { - setEntries(roots); - setNextPageToken(null); - setLoadingEntries(false); - return; - } - - setLoadingEntries(true); - const result = await listContentsAction(activeBackend, currentFolderId, { - // Drive filters server-side; S3 lists the whole level and is filtered client-side. - search: activeBackend === 'drive' && debouncedSearch ? debouncedSearch : undefined, - sortKey, - sortDir, - }); - - // Superseded while in flight — a newer read owns the view now. - if (seq !== listSeq.current) { - return; - } - - if (result.ok) { - setEntries(toViewEntries(result.data?.entries ?? [])); - setNextPageToken(result.data?.nextPageToken ?? null); - } else { - toast.error(result.error ?? 'Failed to open the folder.'); - // Storage disconnected, or the folder was deleted in Drive — refresh so the sidebar reflects it. - if (result.status === 424 || result.status === 404) { - void loadStatus(); - } - } - setLoadingEntries(false); - }, [activeBackend, currentFolderId, roots, toast, loadStatus, debouncedSearch, sortKey, sortDir]); - - useEffect(() => { - void loadEntries(); - }, [loadEntries]); - - const loadMoreEntries = useCallback(async () => { - if ( - nextPageToken === null || - activeBackend === null || - currentFolderId === null || - loadingMore - ) { - return; - } - - const seq = listSeq.current; - setLoadingMore(true); - const result = await listContentsAction(activeBackend, currentFolderId, { - pageToken: nextPageToken, - search: activeBackend === 'drive' && debouncedSearch ? debouncedSearch : undefined, - sortKey, - sortDir, - }); - - // The folder changed under us — appending this page would mix two listings. - if (seq !== listSeq.current) { - setLoadingMore(false); - return; - } - - if (result.ok) { - // Append (never replace) so the scroll position is preserved during infinite scroll. - setEntries((current) => [...current, ...toViewEntries(result.data?.entries ?? [])]); - setNextPageToken(result.data?.nextPageToken ?? null); - } else if (result.status === 424 || result.status === 404) { - void loadStatus(); - } - setLoadingMore(false); - }, [ - nextPageToken, - activeBackend, - currentFolderId, - loadingMore, - debouncedSearch, - sortKey, - sortDir, - loadStatus, - ]); + // Read through a ref: `split` needs reloadPanes, so this breaks the cycle without depending on its identity. + const splitRef = useRef(false); - // Reload both panes after a mutation so the source and destination both refresh. const reloadPanes = useCallback(async () => { - await Promise.all([loadEntries(), split ? paneB.reload() : Promise.resolve()]); - }, [loadEntries, split, paneB]); - - const openFolder = useCallback( - (entry: ViewEntry) => { - if (activeBackend === null) { - return; - } - nameCache.current.set(entry.id, entry.name); - linkCache.current.set(entry.id, entry.webViewLink); - const url = buildWorkspaceUrl(activeBackend, [ - ...path, - { id: entry.id, name: entry.name, webViewLink: entry.webViewLink }, - ]); - window.history.pushState(null, '', url); - }, - [activeBackend, path], - ); - - const navigate = useCallback( - (index: number) => { - if (activeBackend === null) { - return; - } - const url = buildWorkspaceUrl(activeBackend, index < 0 ? [] : path.slice(0, index + 1)); - window.history.pushState(null, '', url); - }, - [activeBackend, path], - ); - - const selectStorage = useCallback((backend: StorageBackend) => { - window.history.pushState(null, '', `/${backend}`); - }, []); + await Promise.all([loadEntries(), splitRef.current ? paneB.reload() : Promise.resolve()]); + }, [loadEntries, paneB]); // The Drive link of the folder currently open in the main pane (null at roots / for S3). const currentFolderLink = path.length > 0 ? path[path.length - 1].webViewLink : null; @@ -521,383 +271,87 @@ function WorkspaceInner({ [openDriveLink, paneBFolderLink], ); - const handleToggleSort = useCallback( - (key: SortKey) => { - const nextDir: SortDir = key === sortKey ? (sortDir === 'asc' ? 'desc' : 'asc') : 'asc'; - const base = activeBackend - ? buildWorkspaceUrl(activeBackend, path) - : window.location.pathname; - // Default (name/asc) stays out of the URL to keep it clean. - const query = key === 'name' && nextDir === 'asc' ? '' : `?sort=${key}&dir=${nextDir}`; - window.history.replaceState(null, '', `${base}${query}`); - }, - [sortKey, sortDir, activeBackend, path], + const { + split, + activePane, + setActivePane, + dragMove, + setDragMove, + toggleSplit, + moveIntoFolder: handleMoveIntoFolder, + moveDrop: handleMoveDrop, + } = useSplitPanes({ + canSplit, + backend: activeBackend, + currentFolderId, + path, + paneB, + reloadPanes, + onMoved: handleMoved, + }); + + // Stable identities: FileRow is memoized, and inline arrows here would make the memo a no-op. + const selectInPane = useMemo( + () => + [0, 1].map((pane) => (entry: ViewEntry) => { + setSelected(entry); + setActivePane(pane as 0 | 1); + }), + [], ); - // --- Split view + drag-to-move ------------------------------------------------ - - const toggleSplit = useCallback(() => { - setSplit((current) => { - const next = !current; - if (next) { - // Open the second pane at the same location as the primary one. - paneB.goTo(path); - } else { - setDragMove(null); - setActivePane(0); - } - return next; - }); - }, [paneB, path]); - - // Close the split (and any drag) if the storage disconnects entirely. - useEffect(() => { - if (activeBackend === null) { - setSplit(false); - setDragMove(null); - } - }, [activeBackend]); - - // Keyboard shortcut (Cmd/Ctrl + \) to toggle the split view. - useEffect(() => { - const handler = (event: KeyboardEvent) => { - if ((event.metaKey || event.ctrlKey) && event.key === '\\') { - const target = event.target as HTMLElement | null; - if (target && (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA')) { - return; - } - // Only open the split from inside a folder; closing is always allowed. - if (!split && currentFolderId === null) { - return; - } - event.preventDefault(); - toggleSplit(); - } - }; - window.addEventListener('keydown', handler); - return () => window.removeEventListener('keydown', handler); - }, [split, currentFolderId, toggleSplit]); - - // Move the given ids into a target folder (Finder-style drop onto a folder row, same pane). - const handleMoveIntoFolder = useCallback( - async (targetFolderId: string, ids: string[]) => { - if (activeBackend === null) { - return; - } - // Can't move a folder into itself. - const moveIds = ids.filter((id) => id !== targetFolderId); - if (moveIds.length === 0) { - return; - } - const backend = activeBackend; - - const count = `${moveIds.length} item${moveIds.length === 1 ? '' : 's'}`; - const toastId = toast.loading(`Moving ${count}…`); - - const results = await Promise.all( - moveIds.map((id) => moveItemAction(backend, id, targetFolderId)), - ); - const failed = results.filter((result) => !result.ok).length; - - await reloadPanes(); - setSelectedIds(new Set()); - paneB.clearSelection(); - if (selected && moveIds.includes(selected.id)) { - setSelected(null); - } - - toast.update(toastId, { - variant: failed > 0 ? 'error' : 'success', - message: - failed > 0 - ? `Failed to move ${failed} item${failed === 1 ? '' : 's'}.` - : `Moved ${count}.`, - }); - }, - [activeBackend, toast, reloadPanes, paneB, selected], + const startMoveDrag = useMemo( + () => [0, 1].map((pane) => (ids: string[]) => setDragMove({ ids, sourcePane: pane as 0 | 1 })), + [], ); - const handleMoveDrop = useCallback( - async (targetPane: 0 | 1) => { - if (!dragMove || activeBackend === null || dragMove.sourcePane === targetPane) { - return; - } - const backend = activeBackend; - const { ids } = dragMove; - const sourceFolderId = dragMove.sourcePane === 0 ? currentFolderId : paneB.currentFolderId; - const targetFolderId = targetPane === 0 ? currentFolderId : paneB.currentFolderId; - setDragMove(null); + const endMoveDrag = useCallback(() => setDragMove(null), []); - if (targetFolderId === null) { - return; - } - if (sourceFolderId === targetFolderId) { - toast.error('Items are already in that folder.'); - return; - } - - const count = `${ids.length} item${ids.length === 1 ? '' : 's'}`; - // A move can take a while (S3 copies whole folders), so show a live toast. - const toastId = toast.loading(`Moving ${count}…`); + splitRef.current = split; - const results = await Promise.all( - ids.map((id) => moveItemAction(backend, id, targetFolderId)), - ); - const failed = results.filter((result) => !result.ok).length; - - await reloadPanes(); - setSelectedIds(new Set()); - paneB.clearSelection(); - if (selected && ids.includes(selected.id)) { - setSelected(null); - } - - if (failed > 0) { - toast.update(toastId, { - variant: 'error', - message: `Failed to move ${failed} item${failed === 1 ? '' : 's'}.`, - }); - } else { - toast.update(toastId, { variant: 'success', message: `Moved ${count}.` }); - } - }, - [dragMove, activeBackend, currentFolderId, paneB, reloadPanes, selected, toast], + // Uploads (duplicate prompt, folder creation, progress, rollback) live in their own hook. + const paneAccess = useMemo( + () => ({ + folderId: (pane: 0 | 1) => (pane === 0 ? currentFolderId : paneB.currentFolderId), + selectedIds: (pane: 0 | 1) => [...(pane === 0 ? selectedIds : paneB.selectedIds)], + clearSelection: (pane: 0 | 1) => (pane === 0 ? clearSelection() : paneB.clearSelection()), + }), + [currentFolderId, paneB, selectedIds, clearSelection], ); - // --- Upload orchestration ----------------------------------------------------- - - // Undo a batch: delete everything it created so a cancel leaves nothing behind. - const rollbackBatch = useCallback( - async (batchId: string, backend: StorageBackend) => { - // Flip the batch and its rows to cancelled (the dock turns them red). - setBatches((current) => - current.map((batch) => - batch.id === batchId - ? { - ...batch, - status: 'canceling', - tasks: batch.tasks.map((task) => ({ ...task, status: 'canceled', rate: null })), - } - : batch, - ), - ); - const runtime = batchRuntime.current.get(batchId); - if (runtime) { - await Promise.all( - [...runtime.rootFolderIds, ...runtime.looseFileIds].map((id) => - deleteItemAction(backend, id), - ), - ); - batchRuntime.current.delete(batchId); - } - await reloadPanes(); - setBatchStatus(batchId, 'canceled'); - scheduleRemoveBatch(batchId); - }, - [reloadPanes, setBatchStatus, scheduleRemoveBatch, setBatches, batchRuntime], + const getPaneTarget = useCallback( + (pane: 0 | 1) => + pane === 0 + ? { folderId: currentFolderId, entries } + : { folderId: paneB.currentFolderId, entries: paneB.entries }, + [currentFolderId, entries, paneB], ); - const askDuplicate = useCallback((names: string[]): Promise<'replace' | 'keep' | 'cancel'> => { - setDuplicate(names); - return new Promise((resolve) => { - duplicateResolve.current = resolve; - }); - }, []); - - const resolveDuplicate = useCallback((choice: 'replace' | 'keep' | 'cancel') => { - setDuplicate(null); - duplicateResolve.current?.(choice); - duplicateResolve.current = null; - }, []); - - const handleUpload = useCallback( - async (droppedItems: UploadItem[], pane: 0 | 1 = 0) => { - const items = droppedItems.filter((item) => !isIgnoredUploadName(item.relativePath)); - const targetFolderId = pane === 0 ? currentFolderId : paneB.currentFolderId; - const existing = pane === 0 ? entries : paneB.entries; - if (targetFolderId === null || activeBackend === null || items.length === 0) { - return; - } - const backend = activeBackend; - - // Detect name conflicts among the top-level items being added here. - const existingNames = new Set(existing.map((entry) => entry.name)); - const topLevel = new Map(); - for (const item of items) { - const top = topLevelName(item.relativePath); - topLevel.set(top, topLevel.get(top) === true || isTopLevelFolder(item.relativePath)); - } - const conflicts = [...topLevel.keys()].filter((name) => existingNames.has(name)); - - const renameMap = new Map(); - // "Replace": upload first, delete originals only once they're safely in. - let replaceIds: string[] = []; - if (conflicts.length > 0) { - const choice = await askDuplicate(conflicts); - if (choice === 'cancel') { - return; - } - if (choice === 'replace') { - replaceIds = existing - .filter((entry) => conflicts.includes(entry.name)) - .map((entry) => entry.id); - } else { - const taken = new Set(existingNames); - for (const name of conflicts) { - const renamed = uniqueName(name, taken); - taken.add(renamed); - renameMap.set(name, renamed); - } - } - } - - // Apply any renames (folders via their path, loose files via an upload name). - const finalItems: UploadItem[] = items.map((item) => { - const top = topLevelName(item.relativePath); - const renamed = renameMap.get(top); - if (!renamed) { - return item; - } - return isTopLevelFolder(item.relativePath) - ? { ...item, relativePath: `${renamed}${item.relativePath.slice(top.length)}` } - : { ...item, relativePath: renamed, uploadName: renamed }; - }); + const uploads = useUploadQueue(activeBackend, getPaneTarget, reloadPanes); - const folderTops = new Set( - finalItems - .filter((item) => isTopLevelFolder(item.relativePath)) - .map((item) => topLevelName(item.relativePath)), - ); - const looseCount = finalItems.filter((item) => !isTopLevelFolder(item.relativePath)).length; - const kind: UploadBatch['kind'] = - folderTops.size === 1 && looseCount === 0 ? 'folder' : 'files'; - const folderName = kind === 'folder' ? [...folderTops][0] : null; - - const batchId = crypto.randomUUID(); - const runtime: BatchRuntime = { - controller: new AbortController(), - looseFileIds: [], - rootFolderIds: [], - }; - batchRuntime.current.set(batchId, runtime); - const { signal } = runtime.controller; - - const queue = finalItems.map((item) => { - const slash = item.relativePath.lastIndexOf('/'); - return { - taskId: crypto.randomUUID(), - item, - dirPath: slash === -1 ? '' : item.relativePath.slice(0, slash), - }; - }); - setBatches((current) => [ - ...current, - { - id: batchId, - kind, - folderName, - status: 'uploading', - tasks: queue.map(({ taskId, item }) => ({ - id: taskId, - name: item.uploadName ?? item.file.name, - size: item.file.size, - percent: 0, - rate: null, - status: 'pending' as const, - })), - }, - ]); - - // Recreate the nested folder structure, tracking top-level folders for cancel. - const folderIdByPath = new Map([['', targetFolderId]]); - const ensureFolder = async (dirPath: string): Promise => { - const cached = folderIdByPath.get(dirPath); - if (cached) { - return cached; - } - const slash = dirPath.lastIndexOf('/'); - const parentPath = slash === -1 ? '' : dirPath.slice(0, slash); - const name = slash === -1 ? dirPath : dirPath.slice(slash + 1); - const parentId = await ensureFolder(parentPath); - const result = await createFolderAction(backend, parentId, name); - if (!result.ok || !result.data) { - throw new Error(result.error ?? 'Failed to create folder.'); - } - const created = result.data; - folderIdByPath.set(dirPath, created.id); - if (parentPath === '') { - runtime.rootFolderIds.push(created.id); - } - return created.id; - }; - - for (const { taskId, item, dirPath } of queue) { - if (signal.aborted) { - break; - } - updateTask(batchId, taskId, { status: 'uploading' }); - try { - // eslint-disable-next-line no-await-in-loop -- uploads run one at a time (progress + folder dedup) - const uploadFolderId = await ensureFolder(dirPath); - // eslint-disable-next-line no-await-in-loop -- sequential upload queue - const result = await uploadFile( - backend, - uploadFolderId, - item.file, - ({ percent, rate }) => { - updateTask(batchId, taskId, { - percent, - rate, - status: percent >= 100 ? 'processing' : 'uploading', - }); - }, - signal, - item.uploadName, - ); - if (dirPath === '') { - runtime.looseFileIds.push(result.fileId); - } - updateTask(batchId, taskId, { status: 'done', percent: 100, rate: null }); - } catch (error) { - if (isCanceledError(error)) { - break; - } - updateTask(batchId, taskId, { status: 'error', rate: null }); - toast.error(`${item.uploadName ?? item.file.name}: ${extractApiErrorMessage(error)}`); - } - } - - if (signal.aborted) { - await rollbackBatch(batchId, backend); - } else { - try { - // Replacements are in — now remove the originals they superseded. - if (replaceIds.length > 0) { - await Promise.all(replaceIds.map((id) => deleteItemAction(backend, id))); - } - await reloadPanes(); - } finally { - batchRuntime.current.delete(batchId); - setBatchStatus(batchId, 'done'); - scheduleRemoveBatch(batchId); - } + /** + * Drops ids from the preview and both panes' selection (after a delete or a changed id). + **/ + const forgetEntries = useCallback( + (ids: string[]) => { + setSelected((current) => (current && ids.includes(current.id) ? null : current)); + for (const id of ids) { + pruneSelection(id); + paneB.pruneSelection(id); } }, - [ - activeBackend, - currentFolderId, - paneB, - entries, - reloadPanes, - toast, - askDuplicate, - updateTask, - setBatchStatus, - scheduleRemoveBatch, - rollbackBatch, - ], + [paneB, pruneSelection], ); + // Entry mutations (new folder, rename, delete, bulk delete, unauthorize) + their dialog state. + const ops = useEntryOperations({ + backend: activeBackend, + panes: paneAccess, + reloadPanes, + loadStatus, + forgetEntries, + }); + const handleDownload = useCallback( (entry: ViewEntry) => { if (activeBackend === null) { @@ -924,215 +378,6 @@ function WorkspaceInner({ [activeBackend], ); - const handleCreateFolder = useCallback( - async (event: FormEvent) => { - event.preventDefault(); - const name = newFolderName.trim(); - const targetFolderId = newFolderPane === 0 ? currentFolderId : paneB.currentFolderId; - if (targetFolderId === null || activeBackend === null || !name || creating) { - return; - } - - setCreating(true); - try { - const result = await createFolderAction(activeBackend, targetFolderId, name); - if (!result.ok) { - toast.error(result.error ?? 'Something went wrong.'); - // The parent is gone (deleted outside the app) — drop it from the sidebar roots. - if (result.status === 424 || result.status === 404) { - void loadStatus(); - } - return; - } - await reloadPanes(); - toast.success(`Folder "${name}" created.`); - setNewFolderOpen(false); - setNewFolderName(''); - } catch (error) { - toast.error(extractApiErrorMessage(error)); - } finally { - setCreating(false); - } - }, - [ - activeBackend, - currentFolderId, - paneB, - newFolderPane, - newFolderName, - creating, - reloadPanes, - toast, - loadStatus, - ], - ); - - /** - * Drops an id from the preview and both panes' selection (after it's deleted or its id changed). - **/ - const forgetEntry = useCallback( - (id: string) => { - setSelected((current) => (current?.id === id ? null : current)); - setSelectedIds((current) => { - if (!current.has(id)) { - return current; - } - const next = new Set(current); - next.delete(id); - return next; - }); - paneB.pruneSelection(id); - }, - [paneB], - ); - - const confirmDelete = useCallback(async () => { - if (!confirmTarget || activeBackend === null) { - return; - } - const entry = confirmTarget; - - setDeleting(true); - try { - const result = await deleteItemAction(activeBackend, entry.id); - if (!result.ok) { - toast.error(result.error ?? 'Something went wrong.'); - return; - } - await reloadPanes(); - forgetEntry(entry.id); - toast.success(`${entry.isFolder ? 'Folder' : 'File'} "${entry.name}" deleted.`); - setConfirmTarget(null); - } catch (error) { - toast.error(extractApiErrorMessage(error)); - } finally { - setDeleting(false); - } - }, [activeBackend, confirmTarget, selected, reloadPanes, paneB, toast]); - - const toggleSelect = useCallback((id: string) => { - setSelectedIds((current) => { - const next = new Set(current); - if (next.has(id)) { - next.delete(id); - } else { - next.add(id); - } - return next; - }); - }, []); - - const selectAll = useCallback(() => { - setSelectedIds(new Set(entries.map((entry) => entry.id))); - }, [entries]); - - const clearSelection = useCallback(() => setSelectedIds(new Set()), []); - - const setSelection = useCallback((ids: string[]) => setSelectedIds(new Set(ids)), []); - - const confirmBulkDelete = useCallback(async () => { - const ids = bulkPane === 0 ? [...selectedIds] : [...paneB.selectedIds]; - if (activeBackend === null || ids.length === 0) { - return; - } - const backend = activeBackend; - - setBulkDeleting(true); - try { - const results = await Promise.all(ids.map((id) => deleteItemAction(backend, id))); - const failed = results.filter((result) => !result.ok).length; - - await reloadPanes(); - if (selected && ids.includes(selected.id)) { - setSelected(null); - } - if (bulkPane === 0) { - setSelectedIds(new Set()); - } else { - paneB.clearSelection(); - } - setBulkDeleteOpen(false); - - if (failed > 0) { - toast.error(`Failed to delete ${failed} item${failed === 1 ? '' : 's'}.`); - } else { - toast.success(`Deleted ${ids.length} item${ids.length === 1 ? '' : 's'}.`); - } - } catch (error) { - toast.error(extractApiErrorMessage(error)); - } finally { - setBulkDeleting(false); - } - }, [activeBackend, bulkPane, selectedIds, paneB, selected, reloadPanes, toast]); - - const openRename = useCallback((entry: ViewEntry) => { - setRenameTarget(entry); - setRenameName(entry.name); - }, []); - - const runRename = useCallback( - async (name: string) => { - if (!renameTarget || activeBackend === null) { - return; - } - // Renaming to the same name is a no-op (and an S3 self-delete) — guard it. - if (name === renameTarget.name) { - setRenameTarget(null); - setExtWarning(null); - return; - } - - setRenaming(true); - setPendingRename(name); - try { - const result = await renameItemAction(activeBackend, renameTarget.id, name); - if (!result.ok) { - toast.error(result.error ?? 'Something went wrong.'); - return; - } - await reloadPanes(); - // The id can change on rename (S3 keys), so drop the old id from selection/preview. - forgetEntry(renameTarget.id); - toast.success(`Renamed to "${name}".`); - setRenameTarget(null); - setExtWarning(null); - } catch (error) { - toast.error(extractApiErrorMessage(error)); - } finally { - setRenaming(false); - setPendingRename(null); - } - }, - [activeBackend, renameTarget, selected, reloadPanes, paneB, toast], - ); - - const handleRename = useCallback( - (event: FormEvent) => { - event.preventDefault(); - const name = renameName.trim(); - if (!renameTarget || activeBackend === null || !name || renaming) { - return; - } - if (name === renameTarget.name) { - setRenameTarget(null); - return; - } - - // Warn (Finder-style) before changing a file's extension. Folders have none. - if (!renameTarget.isFolder) { - const fromExt = splitExtension(renameTarget.name).ext; - const { base, ext: toExt } = splitExtension(name); - if (fromExt.toLowerCase() !== toExt.toLowerCase()) { - setExtWarning({ use: name, keep: `${base}${fromExt}`, fromExt, toExt }); - return; - } - } - - void runRename(name); - }, - [renameName, renameTarget, activeBackend, renaming, runRename], - ); - const handleManageFolders = useCallback(async () => { try { await openPicker(async (folders: PickedFolder[]) => { @@ -1174,27 +419,6 @@ function WorkspaceInner({ } }, [loadStatus, toast]); - const confirmUnselect = useCallback(async () => { - if (!unselectTarget) { - return; - } - setUnselecting(true); - try { - const result = await removeFolderAction(unselectTarget.id); - if (!result.ok) { - toast.error(result.error ?? 'Something went wrong.'); - return; - } - setUnselectTarget(null); - await loadStatus(); - toast.success('Folder removed from the app.'); - } catch (error) { - toast.error(extractApiErrorMessage(error)); - } finally { - setUnselecting(false); - } - }, [unselectTarget, loadStatus, toast]); - const handleLogout = useCallback(() => { setLoggingOut(true); // /logout revokes the session, clears cookies and redirects (full navigation). @@ -1203,24 +427,28 @@ function WorkspaceInner({ const rootLabel = activeStatus?.label ?? 'Home'; const rootIcon = activeBackend ? STORAGE_ICON[activeBackend] : 'Home'; - const bulkCount = bulkPane === 0 ? selectedIds.size : paneB.selectedIds.size; + + // One set of props for both sidebar shells: the desktop rail and the mobile swipe-open drawer. + const sidebarProps: AccountSidebarProps = { + appName, + statuses, + driveStatus, + activeBackend, + loading: loadingStatus, + username, + saving, + isOwner: driveStatus?.isOwner ?? false, + onSelectStorage: selectStorage, + onManageFolders: handleManageFolders, + onDisconnect: handleDisconnect, + onLogout: handleLogout, + loggingOut, + }; return (
    - + +
    {/* Both panes share a grid whose second column animates 1fr↔0fr for a smooth resize. */} @@ -1245,35 +473,26 @@ function WorkspaceInner({ split={split} onToggleSplit={currentFolderId !== null || split ? toggleSplit : undefined} acceptMove={split && dragMove?.sourcePane === 1 && currentFolderId !== null} - onMoveDragStart={(ids) => setDragMove({ ids, sourcePane: 0 })} - onMoveDragEnd={() => setDragMove(null)} + onMoveDragStart={startMoveDrag[0]} + onMoveDragEnd={endMoveDrag} onMoveDrop={() => void handleMoveDrop(0)} onNavigate={navigate} onOpenFolder={openFolder} - onSelect={(entry) => { - setSelected(entry); - setActivePane(0); - }} + onSelect={selectInPane[0]} onDeselect={() => { setSelected(null); clearSelection(); }} - onUpload={(items) => handleUpload(items, 0)} - onNewFolder={() => { - setNewFolderPane(0); - setNewFolderOpen(true); - }} + onUpload={(items) => uploads.handleUpload(items, 0)} + onNewFolder={() => ops.newFolder.openFor(0)} onToggleSelect={toggleSelect} onSelectAll={selectAll} onClearSelection={clearSelection} onSetSelectedIds={setSelection} - onBulkDelete={() => { - setBulkPane(0); - setBulkDeleteOpen(true); - }} + onBulkDelete={() => ops.bulkRemove.openFor(0)} onDownload={handleDownload} - onRename={openRename} - onDelete={setConfirmTarget} + onRename={ops.rename.open} + onDelete={ops.remove.request} storagePicker={{ storages: statuses, activeBackend, @@ -1287,19 +506,22 @@ function WorkspaceInner({ onCopyEntryLink={handleCopyEntryLink} searchQuery={search} onSearchChange={setSearch} - hasMore={nextPageToken !== null} + hasMore={hasMore} loadingMore={loadingMore} onLoadMore={loadMoreEntries} onMoveIntoFolder={handleMoveIntoFolder} onUnselectRoot={ activeBackend === 'drive' && (driveStatus?.isOwner ?? false) - ? setUnselectTarget + ? ops.unselectRoot.request : undefined } />
    + {/* `inert` (not display:none) so the open/close width+opacity transition still plays: + the pane stays laid out, but a closed one is out of the tab order and the a11y tree. */}
    @@ -1317,35 +539,26 @@ function WorkspaceInner({ split={split} onToggleSplit={toggleSplit} acceptMove={split && dragMove?.sourcePane === 0 && paneB.currentFolderId !== null} - onMoveDragStart={(ids) => setDragMove({ ids, sourcePane: 1 })} - onMoveDragEnd={() => setDragMove(null)} + onMoveDragStart={startMoveDrag[1]} + onMoveDragEnd={endMoveDrag} onMoveDrop={() => void handleMoveDrop(1)} onNavigate={paneB.navigate} onOpenFolder={paneB.openFolder} - onSelect={(entry) => { - setSelected(entry); - setActivePane(1); - }} + onSelect={selectInPane[1]} onDeselect={() => { setSelected(null); paneB.clearSelection(); }} - onUpload={(items) => handleUpload(items, 1)} - onNewFolder={() => { - setNewFolderPane(1); - setNewFolderOpen(true); - }} + onUpload={(items) => uploads.handleUpload(items, 1)} + onNewFolder={() => ops.newFolder.openFor(1)} onToggleSelect={paneB.toggleSelect} onSelectAll={paneB.selectAll} onClearSelection={paneB.clearSelection} onSetSelectedIds={paneB.setSelection} - onBulkDelete={() => { - setBulkPane(1); - setBulkDeleteOpen(true); - }} + onBulkDelete={() => ops.bulkRemove.openFor(1)} onDownload={handleDownload} - onRename={openRename} - onDelete={setConfirmTarget} + onRename={ops.rename.open} + onDelete={ops.remove.request} sortKey={paneB.sortKey} sortDir={paneB.sortDir} onToggleSort={paneB.toggleSort} @@ -1368,178 +581,16 @@ function WorkspaceInner({ isRoot={activePane === 0 ? atRoots : paneB.atRoots} backend={activeBackend} onClose={() => setSelected(null)} - onDelete={setConfirmTarget} + onDelete={ops.remove.request} onDownload={handleDownload} - onRename={openRename} + onRename={ops.rename.open} onCopyLink={handleCopyEntryLink} />
    - resolveDuplicate('cancel')} - title='Items already exist'> -
    -

    - {duplicate?.length === 1 - ? 'An item with this name already exists here:' - : `${duplicate?.length} items with these names already exist here:`} -

    -
      - {duplicate?.map((name) => ( -
    • - {name} -
    • - ))} -
    -
    - - - -
    -
    -
    - - setNewFolderOpen(false)} title='New folder'> -
    - setNewFolderName(event.target.value)} - autoFocus - /> -
    - - -
    -
    -
    - - { - setRenameTarget(null); - setExtWarning(null); - }} - title={renameTarget?.isFolder ? 'Rename folder' : 'Rename file'}> -
    - setRenameName(event.target.value)} - autoFocus - /> -
    - - -
    -
    -
    - - {/* Extension-change confirmation (a modal on top of the rename modal) */} - setExtWarning(null)} - title='Change extension?'> -
    -
    -
    - -
    -

    - {extWarning?.toExt === '' - ? `Removing the "${extWarning?.fromExt}" extension may change how this file opens.` - : extWarning?.fromExt === '' - ? `Adding the "${extWarning?.toExt}" extension may change this file's type.` - : `Changing the extension from "${extWarning?.fromExt}" to "${extWarning?.toExt}" may change this file's type.`} -

    -
    -
    - - - -
    -
    -
    - - setBulkDeleteOpen(false)} - title='Delete items' - message={`Delete the ${bulkCount} selected item${bulkCount === 1 ? '' : 's'}, including everything inside any selected folders? This cannot be undone.`} - confirmLabel='Delete' - loading={bulkDeleting} - onConfirm={confirmBulkDelete} - /> - - setConfirmTarget(null)} - title={confirmTarget?.isFolder ? 'Delete folder' : 'Delete file'} - message={ - confirmTarget?.isFolder - ? `Delete the folder "${confirmTarget?.name}" and everything inside it? This cannot be undone.` - : `Delete the file "${confirmTarget?.name}"? This cannot be undone.` - } - confirmLabel='Delete' - loading={deleting} - onConfirm={confirmDelete} - /> - - setUnselectTarget(null)} - title='Remove folder' - message={`Remove "${unselectTarget?.name}" from the app? It stays in Google Drive — only its authorization here is revoked.`} - confirmLabel='Remove' - loading={unselecting} - onConfirm={confirmUnselect} - /> +
    ); } diff --git a/apps/web/src/components/layouts/WorkspaceDialogs.tsx b/apps/web/src/components/layouts/WorkspaceDialogs.tsx new file mode 100644 index 0000000..588e42b --- /dev/null +++ b/apps/web/src/components/layouts/WorkspaceDialogs.tsx @@ -0,0 +1,206 @@ +'use client'; + +import type { EntryOperations } from '@/common/hooks/useEntryOperations'; +import type { UploadQueue } from '@/common/hooks/useUploadQueue'; +import Button from '@/components/common/Button'; +import ConfirmDialog from '@/components/common/ConfirmDialog'; +import Icon from '@/components/common/Icon'; +import Input from '@/components/common/Input'; +import Modal from '@/components/common/Modal'; + +interface Props { + ops: EntryOperations; + uploads: UploadQueue; +} + +/** + * Longest name a folder/file may be given. + **/ +// Matches Drive's UI and every mainstream filesystem (255 bytes); one under the API's `@MaxLength(256)` backstop. +const MAX_NAME_LENGTH = 255; + +/** + * Every workspace dialog: duplicate uploads, new folder, rename, delete, bulk delete and root removal. + **/ +// Purely presentational — state and handlers live in useEntryOperations / useUploadQueue. +export default function WorkspaceDialogs({ ops, uploads }: Props) { + return ( + <> + uploads.resolveDuplicate('cancel')} + title='Items already exist'> +
    +

    + {uploads.duplicate?.length === 1 + ? 'An item with this name already exists here:' + : `${uploads.duplicate?.length} items with these names already exist here:`} +

    +
      + {uploads.duplicate?.map((name) => ( +
    • + {name} +
    • + ))} +
    +
    + + + +
    +
    +
    + + ops.newFolder.close()} title='New folder'> +
    + ops.newFolder.setName(event.target.value)} + autoFocus + /> +
    + + +
    +
    +
    + + { + ops.rename.close(); + ops.rename.dismissExtWarning(); + }} + title={ops.rename.target?.isFolder ? 'Rename folder' : 'Rename file'}> +
    + ops.rename.setName(event.target.value)} + autoFocus + /> +
    + + +
    +
    +
    + + {/* Extension-change confirmation (a modal on top of the rename modal) */} + ops.rename.dismissExtWarning()} + title='Change extension?'> +
    +
    +
    + +
    +

    + {ops.rename.extWarning?.toExt === '' + ? `Removing the "${ops.rename.extWarning?.fromExt}" extension may change how this file opens.` + : ops.rename.extWarning?.fromExt === '' + ? `Adding the "${ops.rename.extWarning?.toExt}" extension may change this file's type.` + : `Changing the extension from "${ops.rename.extWarning?.fromExt}" to "${ops.rename.extWarning?.toExt}" may change this file's type.`} +

    +
    +
    + + + +
    +
    +
    + + ops.bulkRemove.close()} + title='Delete items' + message={`Delete the ${ops.bulkRemove.count} selected item${ops.bulkRemove.count === 1 ? '' : 's'}, including everything inside any selected folders? This cannot be undone.`} + confirmLabel='Delete' + loading={ops.bulkRemove.deleting} + onConfirm={ops.bulkRemove.confirm} + /> + + ops.remove.cancel()} + title={ops.remove.target?.isFolder ? 'Delete folder' : 'Delete file'} + message={ + ops.remove.target?.isFolder + ? `Delete the folder "${ops.remove.target?.name}" and everything inside it? This cannot be undone.` + : `Delete the file "${ops.remove.target?.name}"? This cannot be undone.` + } + confirmLabel='Delete' + loading={ops.remove.deleting} + onConfirm={ops.remove.confirm} + /> + + ops.unselectRoot.cancel()} + title='Remove folder' + message={`Remove "${ops.unselectRoot.target?.name}" from the app? It stays in Google Drive — only its authorization here is revoked.`} + confirmLabel='Remove' + loading={ops.unselectRoot.busy} + onConfirm={ops.unselectRoot.confirm} + /> + + ); +} diff --git a/apps/web/src/components/layouts/mobile/MenuHintModal.tsx b/apps/web/src/components/layouts/mobile/MenuHintModal.tsx new file mode 100644 index 0000000..513e5f1 --- /dev/null +++ b/apps/web/src/components/layouts/mobile/MenuHintModal.tsx @@ -0,0 +1,36 @@ +'use client'; + +import Button from '@/components/common/Button'; +import Icon from '@/components/common/Icon'; +import Modal from '@/components/common/Modal'; + +interface Props { + open: boolean; + onDismiss: () => void; +} + +/** + * First-visit explanation of the swipe-open menu — there is no hamburger to discover. + **/ +export default function MenuHintModal({ open, onDismiss }: Props) { + return ( + +
    + + + + +

    The menu is a swipe away

    + +

    + Swipe right anywhere on the page to open your storage menu, then pick what you need. Swipe + left or tap outside to close it again. +

    + + +
    +
    + ); +} diff --git a/apps/web/src/components/layouts/mobile/MobileDrawer.tsx b/apps/web/src/components/layouts/mobile/MobileDrawer.tsx new file mode 100644 index 0000000..2ee58dd --- /dev/null +++ b/apps/web/src/components/layouts/mobile/MobileDrawer.tsx @@ -0,0 +1,143 @@ +'use client'; + +import { AnimatePresence, motion } from 'motion/react'; +import { useEffect } from 'react'; + +import AccountSidebarContent, { type AccountSidebarProps } from '../AccountSidebarContent'; + +interface Props { + sidebar: AccountSidebarProps; + open: boolean; + onOpen: () => void; + onClose: () => void; +} + +/** How far a horizontal drag has to travel to count as a swipe. */ +const SWIPE_DISTANCE = 70; + +/** How much more horizontal than vertical it has to be, so scrolling never opens the drawer. */ +const SWIPE_RATIO = 1.5; + +/** + * Tells whether a touch started inside something that scrolls sideways. + **/ +// Those elements own their horizontal gestures — a long file name is read by dragging it. +const startedInScroller = (target: EventTarget | null): boolean => { + let node = target instanceof Element ? target : null; + + while (node) { + if (node.scrollWidth > node.clientWidth) { + const { overflowX } = getComputedStyle(node); + + if (overflowX === 'auto' || overflowX === 'scroll') return true; + } + + node = node.parentElement; + } + + return false; +}; + +/** + * The swipe-open account sidebar shown where the desktop rail does not fit. + **/ +export default function MobileDrawer({ sidebar, open, onOpen, onClose }: Props) { + useEffect(() => { + let startX = 0; + let startY = 0; + let tracking = false; + + const onTouchStart = (event: TouchEvent) => { + const touch = event.touches[0]; + + startX = touch.clientX; + startY = touch.clientY; + tracking = !startedInScroller(event.target); + }; + + const onTouchEnd = (event: TouchEvent) => { + if (!tracking) return; + + const touch = event.changedTouches[0]; + const deltaX = touch.clientX - startX; + const deltaY = touch.clientY - startY; + + if (Math.abs(deltaX) < SWIPE_DISTANCE) return; + if (Math.abs(deltaX) < Math.abs(deltaY) * SWIPE_RATIO) return; + + if (deltaX > 0) { + onOpen(); + } else { + onClose(); + } + }; + + window.addEventListener('touchstart', onTouchStart, { passive: true }); + window.addEventListener('touchend', onTouchEnd, { passive: true }); + + return () => { + window.removeEventListener('touchstart', onTouchStart); + window.removeEventListener('touchend', onTouchEnd); + }; + }, [onOpen, onClose]); + + useEffect(() => { + if (!open) return; + + const onKeyDown = (event: KeyboardEvent) => { + if (event.key === 'Escape') onClose(); + }; + + // The page behind must not scroll while the drawer covers it. + const { overflow } = document.body.style; + document.body.style.overflow = 'hidden'; + window.addEventListener('keydown', onKeyDown); + + return () => { + document.body.style.overflow = overflow; + window.removeEventListener('keydown', onKeyDown); + }; + }, [open, onClose]); + + return ( + + {open && ( +
    + + + + {/* Each of these navigates or opens a dialog, so the drawer gets out of the way. */} + { + sidebar.onSelectStorage(backend); + onClose(); + }} + onManageFolders={() => { + sidebar.onManageFolders(); + onClose(); + }} + onDisconnect={() => { + sidebar.onDisconnect(); + onClose(); + }} + /> + +
    + )} +
    + ); +} diff --git a/apps/web/src/components/layouts/mobile/MobileMenu.tsx b/apps/web/src/components/layouts/mobile/MobileMenu.tsx new file mode 100644 index 0000000..978de3a --- /dev/null +++ b/apps/web/src/components/layouts/mobile/MobileMenu.tsx @@ -0,0 +1,60 @@ +'use client'; + +import { useCallback, useEffect, useState } from 'react'; + +import { DESKTOP_QUERY } from '@/common/constants/layout.constants'; +import { useMediaQuery } from '@/common/hooks/useMediaQuery'; + +import type { AccountSidebarProps } from '../AccountSidebarContent'; +import MenuHintModal from './MenuHintModal'; +import MobileDrawer from './MobileDrawer'; + +/** Remembers that the swipe hint has been acknowledged. */ +const HINT_STORAGE_KEY = 'dropto:menu-hint-seen'; + +/** + * Owns the mobile sidebar drawer and its first-visit swipe hint. + **/ +// The media query is the single gate: above `md` the drawer, listener and hint never mount at all. +export default function MobileMenu(sidebar: AccountSidebarProps) { + const hasSidebar = useMediaQuery(DESKTOP_QUERY); + const [menuOpen, setMenuOpen] = useState(false); + const [hintOpen, setHintOpen] = useState(false); + + const openMenu = useCallback(() => setMenuOpen(true), []); + const closeMenu = useCallback(() => setMenuOpen(false), []); + + // First visit on a device without the rail: nobody would guess the swipe on their own. + useEffect(() => { + if (hasSidebar) return; + + try { + if (localStorage.getItem(HINT_STORAGE_KEY)) return; + } catch { + // Storage blocked (private mode, disabled cookies) — show the hint rather than crash. + } + + setHintOpen(true); + }, [hasSidebar]); + + const dismissHint = useCallback(() => { + setHintOpen(false); + + try { + localStorage.setItem(HINT_STORAGE_KEY, '1'); + } catch { + // Storage blocked — the hint simply returns on the next visit. + } + }, []); + + if (hasSidebar) { + return null; + } + + return ( + <> + + + + ); +} diff --git a/apps/web/src/components/providers/ToastProvider.tsx b/apps/web/src/components/providers/ToastProvider.tsx index 6fb8df9..13ce09e 100644 --- a/apps/web/src/components/providers/ToastProvider.tsx +++ b/apps/web/src/components/providers/ToastProvider.tsx @@ -1,7 +1,7 @@ 'use client'; -import { AnimatePresence, motion } from 'framer-motion'; import { CircleAlert, CircleCheck } from 'lucide-react'; +import { AnimatePresence, motion } from 'motion/react'; import { createContext, type PropsWithChildren, diff --git a/apps/web/src/configs/app/app.server-config.ts b/apps/web/src/configs/app/app.server-config.ts index d0df7c5..726dfa3 100644 --- a/apps/web/src/configs/app/app.server-config.ts +++ b/apps/web/src/configs/app/app.server-config.ts @@ -1,6 +1,8 @@ import 'server-only'; interface AppServerConfig { + /** Display name of this instance — the wordmarks, the titles, the manifest and the OG image. */ + name: string; nodeEnv: { isProduction: boolean; }; @@ -10,22 +12,16 @@ interface AppServerConfig { /** Public origin of this web app (canonical / OpenGraph / robots / manifest). */ appUrl: string; }; - /** Auth-cookie domain (production only; a shared parent so app + API share cookies). */ - cookieDomain: string; } -// Server-side config. apiUrl prefers a server-only API_URL, else the public one. +// `name` is read per request and passed as a prop — a NEXT_PUBLIC_* would inline at build and hydrate mismatched. export const appServerConfig: AppServerConfig = { + name: process.env.APP_NAME?.trim() || 'DropTo', nodeEnv: { isProduction: process.env.NODE_ENV === 'production', }, urls: { - apiUrl: ( - process.env.API_URL ?? - process.env.NEXT_PUBLIC_API_URL ?? - 'http://localhost:4000/api/v1' - ).replace(/\/$/, ''), + apiUrl: (process.env.API_URL ?? 'http://localhost:4000/api/v1').replace(/\/$/, ''), appUrl: (process.env.APP_URL ?? 'http://localhost:3000').replace(/\/$/, ''), }, - cookieDomain: process.env.COOKIE_DOMAIN ?? 'localhost', }; diff --git a/apps/web/src/configs/seo/metadata.config.ts b/apps/web/src/configs/seo/metadata.config.ts index c06d761..f607c47 100644 --- a/apps/web/src/configs/seo/metadata.config.ts +++ b/apps/web/src/configs/seo/metadata.config.ts @@ -1,3 +1,5 @@ +import { appServerConfig } from '@/configs/app/app.server-config'; + interface MetadataConfig { title: string; shortTitle: string; @@ -11,15 +13,15 @@ interface MetadataConfig { }; } -// Static site metadata for the layout, manifest and robots. +// Site metadata for the layout, manifest, robots and the OG image (name from the env). export const metadataConfig: MetadataConfig = { - title: 'DropTo', - shortTitle: 'DropTo', + title: appServerConfig.name, + shortTitle: appServerConfig.name, tagline: 'Self-hosted file uploader', description: 'Self-hosted file uploader that streams your uploads straight into your own storage — Google Drive or S3-compatible buckets — from a fast, Finder-like workspace.', keywords: [ - 'DropTo', + appServerConfig.name, 'self-hosted file uploader', 'file uploader', 'file manager', diff --git a/apps/web/src/proxy.ts b/apps/web/src/proxy.ts index 7ffca9f..fe1f7ad 100644 --- a/apps/web/src/proxy.ts +++ b/apps/web/src/proxy.ts @@ -99,12 +99,10 @@ export async function proxy(request: NextRequest): Promise { return NextResponse.next(); } - // No refresh token at all → not logged in. if (!refreshToken) { return redirectToLogin(request, false); } - // Access token still valid → no work. if (accessFresh) { return NextResponse.next(); } diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json index 2063a5f..ab937a3 100644 --- a/apps/web/tsconfig.json +++ b/apps/web/tsconfig.json @@ -27,5 +27,7 @@ ".next/types/**/*.ts", ".next/dev/types/**/*.ts" ], - "exclude": ["node_modules"] + // prettier.config.ts extends the workspace config, which is outside the Docker build + // context; it is tooling config, not app code, so keep it out of the Next type-check. + "exclude": ["node_modules", "prettier.config.ts"] } diff --git a/docker-compose.infra.yml b/docker-compose.infra.yml deleted file mode 100644 index 84c543c..0000000 --- a/docker-compose.infra.yml +++ /dev/null @@ -1,53 +0,0 @@ -# docker-compose.infra.yml -# Infrastructure services only — for local development where apps run natively. -# -# Usage: -# docker compose -f docker-compose.infra.yml up -d -# pnpm dev # apps run directly and connect to localhost ports - -services: - dropto-pg: - image: postgres:17-alpine - container_name: dropto-pg - restart: unless-stopped - environment: - POSTGRES_USER: '${DB_USER}' - POSTGRES_PASSWORD: '${DB_PASS}' - POSTGRES_DB: '${DB_NAME}' - ports: - - '127.0.0.1:${DB_PORT}:5432' - volumes: - - dropto_pg_data:/var/lib/postgresql/data - networks: - - dropto_net - healthcheck: - test: ['CMD-SHELL', 'pg_isready -U ${DB_USER} -d ${DB_NAME}'] - interval: 10s - timeout: 5s - retries: 5 - - dropto-redis: - image: redis:7-alpine - container_name: dropto-redis - restart: unless-stopped - command: ['redis-server', '--requirepass', '${REDIS_PASS}'] - ports: - - '127.0.0.1:${REDIS_PORT}:6379' - volumes: - - dropto_redis_data:/data - networks: - - dropto_net - healthcheck: - test: ['CMD', 'redis-cli', '-a', '${REDIS_PASS}', 'ping'] - interval: 10s - timeout: 5s - retries: 5 - -volumes: - dropto_pg_data: - dropto_redis_data: - -networks: - dropto_net: - driver: bridge - name: dropto_net diff --git a/docker-compose.yml b/docker-compose.yml index b0b8c5e..8bbf48b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,13 +1,18 @@ # docker-compose.yml -# Runs the full infrastructure + all applications. +# Full deployment: Postgres + Redis + the API + the web app. # # Usage: -# docker compose up -d # all services -# docker compose up -d dropto-pg # infrastructure only -# docker compose logs -f dropto-api # stream logs for a service +# cp .env.example .env # infra + web settings +# cp apps/api/.env.example apps/api/.env # API secrets +# docker compose up -d --build # build + start everything +# docker compose logs -f dropto-web # stream logs for a service +# docker compose down # stop (data survives in the volumes) # -# Infrastructure + web build args: root .env (see .env.example) -# API env vars: apps/api/.env (see apps/api/.env.example) +# Migrations run automatically on every `up` (the one-shot `dropto-migrate` service); the API +# only starts once they have been applied. +# +# Infra + web settings: root .env (see .env.example) +# API secrets: apps/api/.env (see apps/api/.env.example) services: dropto-pg: @@ -19,7 +24,8 @@ services: POSTGRES_PASSWORD: '${DB_PASS}' POSTGRES_DB: '${DB_NAME}' ports: - - '127.0.0.1:${DB_PORT}:5432' + # Loopback only — nothing outside this host needs to reach the database. + - '127.0.0.1:${DB_PORT:-5432}:5432' volumes: - dropto_pg_data:/var/lib/postgresql/data networks: @@ -36,7 +42,7 @@ services: restart: unless-stopped command: ['redis-server', '--requirepass', '${REDIS_PASS}'] ports: - - '127.0.0.1:${REDIS_PORT}:6379' + - '127.0.0.1:${REDIS_PORT:-6379}:6379' volumes: - dropto_redis_data:/data networks: @@ -47,20 +53,59 @@ services: timeout: 5s retries: 5 - dropto-api: + # One-shot: applies pending Prisma migrations, then exits. Shares the API image. + dropto-migrate: build: context: . dockerfile: apps/api/Dockerfile - container_name: dropto-api - restart: unless-stopped + image: dropto-api + container_name: dropto-migrate + restart: 'no' + command: ['node_modules/.bin/prisma', 'migrate', 'deploy'] env_file: - apps/api/.env - environment: + environment: &api_env NODE_ENV: 'production' + # Inside the network the services talk over their container names and default ports; these + # override apps/api/.env so the root .env stays the single source for infra credentials. DB_HOST: 'dropto-pg' + DB_PORT: '5432' + DB_NAME: '${DB_NAME}' + DB_USER: '${DB_USER}' + DB_PASS: '${DB_PASS}' REDIS_HOST: 'dropto-redis' + REDIS_PORT: '6379' + REDIS_USER: '${REDIS_USER:-default}' + REDIS_PASS: '${REDIS_PASS}' + REDIS_KEY_PREFIX: '${REDIS_KEY_PREFIX:-dropto:}' + networks: + - dropto_net + depends_on: + dropto-pg: + condition: service_healthy + + dropto-api: + build: + context: . + dockerfile: apps/api/Dockerfile + image: dropto-api + container_name: dropto-api + restart: unless-stopped + env_file: + - apps/api/.env + environment: *api_env + # The server only reads its own bundle; /tmp is writable for Prisma's query engine. + read_only: true + tmpfs: + - /tmp + cap_drop: + - ALL + security_opt: + - no-new-privileges:true ports: - - '4000:4000' + # The browser hits this directly during the Google OAuth redirect, so it must be reachable + # from wherever the operator's browser runs. Drop the 127.0.0.1 prefix to expose it on the LAN. + - '127.0.0.1:${API_PORT:-4000}:4000' networks: - dropto_net depends_on: @@ -68,40 +113,45 @@ services: condition: service_healthy dropto-redis: condition: service_healthy - healthcheck: - test: ['CMD-SHELL', 'wget -qO- http://localhost:4000/health || exit 1'] - interval: 10s - timeout: 5s - retries: 5 + dropto-migrate: + condition: service_completed_successfully dropto-web: build: context: . dockerfile: apps/web/Dockerfile args: - NEXT_PUBLIC_API_URL: '${NEXT_PUBLIC_API_URL:-http://localhost:4000/api/v1}' + # Inlined into the client bundle / prerendered metadata — build-time only. NEXT_PUBLIC_GOOGLE_API_KEY: '${NEXT_PUBLIC_GOOGLE_API_KEY:-}' NEXT_PUBLIC_GOOGLE_APP_ID: '${NEXT_PUBLIC_GOOGLE_APP_ID:-}' APP_URL: '${APP_URL:-http://localhost:3000}' + image: dropto-web container_name: dropto-web restart: unless-stopped + read_only: true + tmpfs: + - /tmp + cap_drop: + - ALL + security_opt: + - no-new-privileges:true environment: NODE_ENV: 'production' PORT: '3000' + # Runtime, so renaming the instance needs no rebuild. + APP_NAME: '${APP_NAME:-DropTo}' + APP_URL: '${APP_URL:-http://localhost:3000}' + # Server-to-server, over the compose network — never leaves the host. API_URL: 'http://dropto-api:4000/api/v1' COOKIE_DOMAIN: '${COOKIE_DOMAIN:-localhost}' ports: - - '3000:3000' + # Bound to loopback for a same-host reverse proxy; drop the prefix to expose it directly. + - '127.0.0.1:${WEB_PORT:-3000}:3000' networks: - dropto_net depends_on: dropto-api: condition: service_healthy - healthcheck: - test: ['CMD-SHELL', 'wget -qO- http://localhost:3000 || exit 1'] - interval: 10s - timeout: 5s - retries: 5 volumes: dropto_pg_data: diff --git a/eslint.config.base.mjs b/eslint.config.base.mjs index 2b49b37..eb14a94 100644 --- a/eslint.config.base.mjs +++ b/eslint.config.base.mjs @@ -1,6 +1,5 @@ import tsPlugin from '@typescript-eslint/eslint-plugin'; import tsParser from '@typescript-eslint/parser'; -import importPlugin from 'eslint-plugin-import'; import simpleImportSort from 'eslint-plugin-simple-import-sort'; import unusedImports from 'eslint-plugin-unused-imports'; @@ -8,7 +7,6 @@ import unusedImports from 'eslint-plugin-unused-imports'; export const parser = tsParser; export const plugins = { - import: importPlugin, 'unused-imports': unusedImports, 'simple-import-sort': simpleImportSort, '@typescript-eslint': tsPlugin, @@ -41,5 +39,4 @@ export const rules = { { groups: [['^\\u0000', '^@?\\w'], ['^@/'], ['^\\.'], ['^.+\\.(css|scss)$']] }, ], 'simple-import-sort/exports': 'error', - 'import/no-named-as-default-member': 'warn', }; diff --git a/package.json b/package.json index 1b5e4a7..2cd705b 100644 --- a/package.json +++ b/package.json @@ -1,16 +1,15 @@ { "name": "dropto", - "version": "1.0.0", "description": "Self-hosted file uploader that lets you route uploads through your own storage backends (Google Drive, S3, and more) instead of your app server's disk.", "private": true, "author": { "name": "Petr Kašpar", "url": "https://github.com/ksprptr" }, - "packageManager": "pnpm@10.34.4", + "packageManager": "pnpm@11.5.1", "engines": { - "node": ">=22", - "pnpm": ">=10" + "node": ">=24", + "pnpm": ">=11" }, "scripts": { "dev": "turbo dev", @@ -18,18 +17,17 @@ "lint": "turbo lint", "test": "turbo test", "format": "prettier --write \"**/*.{ts,tsx,json,md}\"", - "prepare": "husky" + "prepare": "husky || true" }, "devDependencies": { - "@typescript-eslint/eslint-plugin": "^8.65.0", - "@typescript-eslint/parser": "^8.65.0", - "eslint-plugin-import": "^2.32.0", + "@typescript-eslint/eslint-plugin": "^8.68.0", + "@typescript-eslint/parser": "^8.68.0", "eslint-plugin-simple-import-sort": "^14.0.0", "eslint-plugin-unused-imports": "^4.4.1", "husky": "^9.1.7", "prettier": "^3.9.6", "prettier-plugin-tailwindcss": "^0.8.1", - "turbo": "^2.10.7", + "turbo": "^2.10.12", "typescript": "^6.0.3" } } diff --git a/packages/types/index.ts b/packages/types/index.ts index 60918f5..d340ef4 100644 --- a/packages/types/index.ts +++ b/packages/types/index.ts @@ -50,7 +50,7 @@ export interface ResumableUploadSession { } /** How far a resumable session got — so a dropped upload resumes from `receivedBytes` (never re-uploads whole). */ -export interface UploadStatus { +export interface ResumableUploadStatus { complete: boolean; receivedBytes: number; fileId: string | null; diff --git a/packages/types/package.json b/packages/types/package.json index daa671d..041b94d 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -1,6 +1,5 @@ { "name": "@dropto/types", - "version": "1.0.0", "private": true, "main": "./index.ts", "types": "./index.ts", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bf0bff8..42557c7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,8 +8,10 @@ overrides: google-auth-library: 10.9.0 sharp: ^0.35.3 postcss: ^8.5.23 - fast-uri@3: 3.1.4 - brace-expansion: 5.0.8 + fast-uri@3: 3.1.5 + brace-expansion: 5.0.9 + cross-spawn: ^7.0.6 + tmp: ^0.2.6 '@hono/node-server': ^1.19.17 valibot: ^1.4.2 js-yaml@5: 5.2.2 @@ -20,20 +22,17 @@ importers: .: devDependencies: '@typescript-eslint/eslint-plugin': - specifier: ^8.65.0 - version: 8.65.0(@typescript-eslint/parser@8.65.0(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3) + specifier: ^8.68.0 + version: 8.68.0(@typescript-eslint/parser@8.68.0(eslint@10.9.1(jiti@2.7.0))(typescript@6.0.3))(eslint@10.9.1(jiti@2.7.0))(typescript@6.0.3) '@typescript-eslint/parser': - specifier: ^8.65.0 - version: 8.65.0(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3) - eslint-plugin-import: - specifier: ^2.32.0 - version: 2.32.0(@typescript-eslint/parser@8.65.0(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.8.0(jiti@2.7.0)) + specifier: ^8.68.0 + version: 8.68.0(eslint@10.9.1(jiti@2.7.0))(typescript@6.0.3) eslint-plugin-simple-import-sort: specifier: ^14.0.0 - version: 14.0.0(eslint@10.8.0(jiti@2.7.0)) + version: 14.0.0(eslint@10.9.1(jiti@2.7.0)) eslint-plugin-unused-imports: specifier: ^4.4.1 - version: 4.4.1(@typescript-eslint/eslint-plugin@8.65.0(@typescript-eslint/parser@8.65.0(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.8.0(jiti@2.7.0)) + version: 4.4.1(@typescript-eslint/eslint-plugin@8.68.0(@typescript-eslint/parser@8.68.0(eslint@10.9.1(jiti@2.7.0))(typescript@6.0.3))(eslint@10.9.1(jiti@2.7.0))(typescript@6.0.3))(eslint@10.9.1(jiti@2.7.0)) husky: specifier: ^9.1.7 version: 9.1.7 @@ -44,8 +43,8 @@ importers: specifier: ^0.8.1 version: 0.8.1(prettier@3.9.6) turbo: - specifier: ^2.10.7 - version: 2.10.7 + specifier: ^2.10.12 + version: 2.10.12 typescript: specifier: ^6.0.3 version: 6.0.3 @@ -53,38 +52,41 @@ importers: apps/api: dependencies: '@aws-sdk/client-s3': - specifier: ^3.1098.0 - version: 3.1098.0 + specifier: ^3.1121.0 + version: 3.1121.0 '@aws-sdk/lib-storage': - specifier: ^3.1098.0 - version: 3.1098.0(@aws-sdk/client-s3@3.1098.0) + specifier: ^3.1121.0 + version: 3.1121.0(@aws-sdk/client-s3@3.1121.0) + '@dropto/types': + specifier: workspace:* + version: link:../../packages/types '@nestjs/common': - specifier: ^11.1.28 - version: 11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) + specifier: ^12.0.1 + version: 12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nestjs/config': - specifier: ^4.0.4 - version: 4.0.4(@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(rxjs@7.8.2) + specifier: ^12.0.0 + version: 12.0.0(@nestjs/common@12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(rxjs@7.8.2) '@nestjs/core': - specifier: ^11.1.28 - version: 11.1.28(@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.28)(reflect-metadata@0.2.2)(rxjs@7.8.2) + specifier: ^12.0.1 + version: 12.0.1(@nestjs/common@12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@12.0.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nestjs/jwt': - specifier: ^11.0.2 - version: 11.0.2(@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)) + specifier: ^12.0.1 + version: 12.0.1(@nestjs/common@12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)) '@nestjs/platform-express': - specifier: ^11.1.28 - version: 11.1.28(@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.28) + specifier: ^12.0.1 + version: 12.0.1(@nestjs/common@12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@12.0.1) '@nestjs/swagger': - specifier: ^11.4.6 - version: 11.4.6(@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.28)(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2) + specifier: ^12.0.1 + version: 12.0.1(@nestjs/common@12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@12.0.1)(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2) '@nestjs/terminus': specifier: ^11.1.1 - version: 11.1.1(@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.28)(@prisma/client@7.9.1(prisma@7.9.1(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@6.0.3))(typescript@6.0.3))(reflect-metadata@0.2.2)(rxjs@7.8.2) + version: 11.1.1(@nestjs/common@12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@12.0.1)(@prisma/client@7.10.0(prisma@7.10.0(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@6.0.3))(typescript@6.0.3))(reflect-metadata@0.2.2)(rxjs@7.8.2) '@prisma/adapter-pg': - specifier: ^7.9.1 - version: 7.9.1 + specifier: ^7.10.0 + version: 7.10.0 '@prisma/client': - specifier: ^7.9.1 - version: 7.9.1(prisma@7.9.1(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@6.0.3))(typescript@6.0.3) + specifier: ^7.10.0 + version: 7.10.0(prisma@7.10.0(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@6.0.3))(typescript@6.0.3) archiver: specifier: ^8.0.0 version: 8.0.0 @@ -110,17 +112,17 @@ importers: specifier: ^5.2.1 version: 5.2.1 googleapis: - specifier: ^173.0.0 - version: 173.0.0 + specifier: ^176.0.0 + version: 176.0.0 helmet: specifier: ^8.3.0 version: 8.3.0 ioredis: - specifier: ^5.11.1 - version: 5.11.1 + specifier: ^6.0.0 + version: 6.0.0 prisma: - specifier: ^7.9.1 - version: 7.9.1(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@6.0.3) + specifier: ^7.10.0 + version: 7.10.0(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@6.0.3) rate-limiter-flexible: specifier: ^11.2.0 version: 11.2.0 @@ -132,14 +134,14 @@ importers: version: 7.8.2 devDependencies: '@nestjs/cli': - specifier: ^11.0.24 - version: 11.0.24(@types/node@26.1.2)(prettier@3.9.6) + specifier: ^12.0.0 + version: 12.0.0(@types/node@26.4.0)(fork-ts-checker-webpack-plugin@9.1.0(typescript@6.0.3)(webpack@5.106.2))(prettier@3.9.6)(tsconfig-paths-webpack-plugin@4.2.0)(webpack-node-externals@3.0.0)(webpack@5.106.2) '@nestjs/schematics': - specifier: ^11.1.0 - version: 11.1.0(chokidar@4.0.3)(prettier@3.9.6)(typescript@6.0.3) + specifier: ^12.0.0 + version: 12.0.0(chokidar@5.0.0)(prettier@3.9.6)(typescript@6.0.3) '@nestjs/testing': - specifier: ^11.1.28 - version: 11.1.28(@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.28)(@nestjs/platform-express@11.1.28) + specifier: ^12.0.1 + version: 12.0.1(@nestjs/common@12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@12.0.1)(@nestjs/platform-express@12.0.1) '@types/archiver': specifier: ^8.0.0 version: 8.0.0 @@ -156,17 +158,17 @@ importers: specifier: ^30.0.0 version: 30.0.0 '@types/node': - specifier: ^26.1.2 - version: 26.1.2 + specifier: ^26.4.0 + version: 26.4.0 '@types/supertest': specifier: ^7.2.1 version: 7.2.1 eslint: - specifier: ^10.8.0 - version: 10.8.0(jiti@2.7.0) + specifier: ^10.9.1 + version: 10.9.1(jiti@2.7.0) jest: - specifier: ^30.4.2 - version: 30.4.2(@types/node@26.1.2)(ts-node@10.9.2(@types/node@26.1.2)(typescript@6.0.3)) + specifier: ^30.5.0 + version: 30.5.0(@types/node@26.4.0)(ts-node@10.9.2(@types/node@26.4.0)(typescript@6.0.3)) prisma-dbml-generator: specifier: ^0.12.0 version: 0.12.0 @@ -175,10 +177,10 @@ importers: version: 7.2.2 ts-jest: specifier: ^29.4.12 - version: 29.4.12(@babel/core@7.29.7)(@jest/transform@30.4.1)(@jest/types@30.4.1)(babel-jest@30.4.1(@babel/core@7.29.7))(jest-util@30.4.1)(jest@30.4.2(@types/node@26.1.2)(ts-node@10.9.2(@types/node@26.1.2)(typescript@6.0.3)))(typescript@6.0.3) + version: 29.4.12(@babel/core@7.29.7)(@jest/transform@30.5.0)(@jest/types@30.5.0)(babel-jest@30.5.0(@babel/core@7.29.7))(jest-util@30.5.0)(jest@30.5.0(@types/node@26.4.0)(ts-node@10.9.2(@types/node@26.4.0)(typescript@6.0.3)))(typescript@6.0.3) ts-node: specifier: ^10.9.2 - version: 10.9.2(@types/node@26.1.2)(typescript@6.0.3) + version: 10.9.2(@types/node@26.4.0)(typescript@6.0.3) tsconfig-paths: specifier: ^4.2.0 version: 4.2.0 @@ -192,17 +194,17 @@ importers: specifier: workspace:* version: link:../../packages/types axios: - specifier: ^1.19.0 - version: 1.19.0 - framer-motion: - specifier: ^12.43.0 - version: 12.43.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + specifier: ^1.20.0 + version: 1.20.0 lucide-react: - specifier: ^1.28.0 - version: 1.28.0(react@19.2.8) + specifier: ^1.38.0 + version: 1.38.0(react@19.2.8) + motion: + specifier: ^13.1.1 + version: 13.1.1(react-dom@19.2.8(react@19.2.8))(react@19.2.8) next: - specifier: 16.2.12 - version: 16.2.12(@opentelemetry/api@1.4.1)(@types/node@26.1.2)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + specifier: 16.3.3 + version: 16.3.3(@opentelemetry/api@1.4.1)(@types/node@26.4.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) next-themes: specifier: ^0.4.6 version: 0.4.6(react-dom@19.2.8(react@19.2.8))(react@19.2.8) @@ -220,17 +222,17 @@ importers: specifier: ^4.3.3 version: 4.3.3 '@types/node': - specifier: ^26.1.2 - version: 26.1.2 + specifier: ^26.4.0 + version: 26.4.0 '@types/react': - specifier: ^19.2.17 - version: 19.2.17 + specifier: ^19.2.18 + version: 19.2.18 '@types/react-dom': - specifier: ^19.2.3 - version: 19.2.3(@types/react@19.2.17) + specifier: ^19.2.5 + version: 19.2.5(@types/react@19.2.18) eslint: - specifier: ^10.8.0 - version: 10.8.0(jiti@2.7.0) + specifier: ^10.9.1 + version: 10.9.1(jiti@2.7.0) prettier: specifier: ^3.9.6 version: 3.9.6 @@ -256,113 +258,100 @@ packages: resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} engines: {node: '>=10'} - '@angular-devkit/core@19.2.24': - resolution: {integrity: sha512-Kd49warf6U/EyWe5BszF/eebN3zQ3bk7tgfEljAw8q/rX95UUtriJubWvp6pgzHfzBA4jwq8f+QiNZB8eBEXPA==} - engines: {node: ^18.19.1 || ^20.11.1 || >=22.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} - peerDependencies: - chokidar: ^4.0.0 - peerDependenciesMeta: - chokidar: - optional: true - - '@angular-devkit/core@19.2.27': - resolution: {integrity: sha512-3amNzoCVSKd7ah6l6lBQL4onwwJvqvam7FMoQBILrxtW5LB5ezh8gMSPuA4zJjKjoRzf9uoWdlzqv/84I52xZA==} - engines: {node: ^18.19.1 || ^20.11.1 || >=22.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} + '@angular-devkit/core@22.1.5': + resolution: {integrity: sha512-HiY6d5dkIdJs5grP9OHvgkf14QOcDIo+hbuT7YKuLItQ++ZxCwUabJMLCCJ5R0KrstE5NqED0dNcyk5WD01t5Q==} + 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: ^4.0.0 + chokidar: ^5.0.0 peerDependenciesMeta: chokidar: optional: true - '@angular-devkit/schematics-cli@19.2.27': - resolution: {integrity: sha512-wHYH6SVXVykhLzovUHtYor3Nl4SpIiITi7r9DQDaKYUD4hpRBx25W6N9eGuakT9Vd5tV/x6wmvQFWQZQwFB7eA==} - engines: {node: ^18.19.1 || ^20.11.1 || >=22.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} + '@angular-devkit/schematics-cli@22.1.5': + resolution: {integrity: sha512-VB/5fH2b/2j8eeMm25vRYCJzBbpaM/VyHc8/+WqPzJahdWuvL0CeLWrxjHHT1exQ/kgbqHLaIWMgSywZe3EBhA==} + 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'} hasBin: true - '@angular-devkit/schematics@19.2.24': - resolution: {integrity: sha512-lnw+ZM1Io+cJAkReC0NPDjqObL8NtKzKIkdgEEKC8CUmkhurYhedbicN8Y8NYHgG1uLd2GozW3+/QqPRZaN+Lw==} - engines: {node: ^18.19.1 || ^20.11.1 || >=22.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} - - '@angular-devkit/schematics@19.2.27': - resolution: {integrity: sha512-/PZmyAlb2NGWPikRRuiWLdfHQd8Wrx6lX4HqvTcaDhlU43M3T0ud4PH2T3QDp7BzHYY92xtD8iPxX2asg67G1A==} - engines: {node: ^18.19.1 || ^20.11.1 || >=22.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} + '@angular-devkit/schematics@22.1.5': + resolution: {integrity: sha512-HTmo9y8wjXKtJGVTYomTjZsjWzhirpu1pPRAzsVob3eiYOuxv1qDRAgWiHXNGp5CpnbHunZweXY/WffNGOFRyA==} + 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'} '@antfu/ni@0.21.4': resolution: {integrity: sha512-O0Uv9LbLDSoEg26fnMDdDRiPwFJnQSoD4WnrflDwKCJm8Cx/0mV4cGxwBLXan5mGIrpK4Dd7vizf4rQm0QCEAA==} hasBin: true - '@aws-sdk/checksums@3.1000.23': - resolution: {integrity: sha512-Hv4VX5+IdDYmAbOdiwZguz8fLh3WnE4VtyJwVNEHLulA+OYy7Z5L0ETGUlX2T4g8DLO8XxCV8CyWrtVL4uwqkg==} + '@aws-sdk/checksums@3.1000.29': + resolution: {integrity: sha512-Dtu0gr4dnATZAPwEYbpCsG+MpLM7OAliy2gTepEFQwl1vZ6DL3QMH2FveMa3HLvPsOdhJsPRB3KtxVhph9T75A==} engines: {node: '>=20.0.0'} - '@aws-sdk/client-s3@3.1098.0': - resolution: {integrity: sha512-6Z53QG2jdujsCt3eHO2cjCpdfvz2Pgs+hP/NINuGONZUJmdrHLLDvWM9F114Yq0NPIGtk+yA/GCjG3XiUhw+gA==} + '@aws-sdk/client-s3@3.1121.0': + resolution: {integrity: sha512-hBnoqaVBeWdkgXcJElMXA2yUZWkBCBntu2qmN+tfqmzC+j4LzJC3ox8qIgS2WdMS1cb8UwyBogUVrkRXybNm0A==} engines: {node: '>=20.0.0'} - '@aws-sdk/core@3.977.3': - resolution: {integrity: sha512-6YZTpF5Zzl0KdSrztM0l6ErKdnXrnnodIDYfayHE9SFfZU8Ubxls7H2XnfWT121jgwiG+WP1z5kyfVDsH5V4qA==} + '@aws-sdk/core@3.977.9': + resolution: {integrity: sha512-reqPFEQrZxDZpeGj4PFMepBeR5LGYHRqq/L0motTzgFkCRBA4rFdaVXDSLYyGHhxVz7sT2PDnPN9CluGSfgyJA==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-env@3.972.64': - resolution: {integrity: sha512-14kkR5aj1c7D+cYCrEcG2W3xw87wMYAEUeB/tMiQ2j0RVKzzdewd4mJw1+Q0JVLr4IFU1JHGDCyj0WqLrtIixg==} + '@aws-sdk/credential-provider-env@3.972.70': + resolution: {integrity: sha512-H404B7dJl2mCrBqahDEYsanB0xhdDp6tXnXcTUnXmmpy2Q3J0Ho0bUajZ2jr/RdwzCyS59Gi8xXIFwPLGBl6Uw==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-http@3.972.66': - resolution: {integrity: sha512-BrZxoXScBZ+nTOYy388zHkz1n5cS8C+uGFMozD4w9OKEWMq39Cu/9l/k385Hb54dtv0VIeh12f8h67o3xDNH9g==} + '@aws-sdk/credential-provider-http@3.972.72': + resolution: {integrity: sha512-X98zYOrVOeuosCX+6ktf29FC2N2GHPLia7qv6mzPzTc+RPAuHWCDS++Z6JK7eGYqb/v6uaW7bAXaOvDBfol+0w==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-ini@3.973.9': - resolution: {integrity: sha512-d/mMvS+sQjSWNA6+JCldK1YB/jQmtoWM8Izj10mrOZdRCeWNVb8IYQuuyHHKKKXcpGo5Pd0LIK6onHbLvlhuVQ==} + '@aws-sdk/credential-provider-ini@3.973.15': + resolution: {integrity: sha512-Rykg6s5ceBuynMOGWgoowO4N+27JfnqXAnVaSunZl0hOO1XodSrxGNz6sCEbnmS0lAfQZDKyb3fbr46gSuv6Sg==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-login@3.972.71': - resolution: {integrity: sha512-l93922lXnTs5RjM3QrMCXSmXiEgLFLaQ0Lco9F3tJ08o0mzGdAj6m2nAXDuTMHoUuL0u7RKmrhm+Z8/7GCilyg==} + '@aws-sdk/credential-provider-login@3.972.77': + resolution: {integrity: sha512-Jb59xfEISoN5mmbnA+HYqdtrSX3CgCtJoof+V5D8/TgUI56W63GEEd5Y58WijU3Ou6+WEgaLD1feVzaRXV5IDQ==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-node@3.972.75': - resolution: {integrity: sha512-sdFhR4E3HWZefGL6OsBhxqXdqtFvxGOh8VLfiu9yttPgVNcpEozEDPmrTYmXA3CIf6npUETVLveQPGQ7GBzVhg==} + '@aws-sdk/credential-provider-node@3.972.81': + resolution: {integrity: sha512-Rml+WitoFvXmv6JZ18U/xGdGDGGvB/mOin0ya0lTnTrdC0Z1lrVxTYh7iNklZBcvcRMrs4DoEf6xy1KWyrLQQw==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-process@3.972.64': - resolution: {integrity: sha512-Q5noG5vfFo++bUaLGcjj8OHX5tmwa1O/5nDVMeaSSZgjzgvWbW6tgu/gSa+aENwcJXjoXDQh+8TRZR2zHds/aw==} + '@aws-sdk/credential-provider-process@3.972.70': + resolution: {integrity: sha512-2ry03fGRJr4sV3jI+ocjj5JqALnFD6ymM5KiNCDZMvq8bX2GSbE0vji4aM43TVCl2nXqqLRZaUxdq/KeWRAY4Q==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-sso@3.973.8': - resolution: {integrity: sha512-37okeZyRdVkGrU6nyKBPGqQvyl/7CEiMpUVOb0+BCNCIR0VAuBxNgjMb0mnsR1EXm8dqQFfQZOJ5YW/0sBfa1w==} + '@aws-sdk/credential-provider-sso@3.973.14': + resolution: {integrity: sha512-jkhg/8ocAAoc0RFyLMhCw+/zZh7gystQgd4F4hznNa8P4Cc501PQmxd+jGLiMHodPJ+7Zv/3znM62gZojyasmA==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-web-identity@3.972.70': - resolution: {integrity: sha512-Ps/f9USafScb6CSQTRPNMzdKL5x5XRwDIRgFvnuFWfClGZe7/fI7iCFqXdxKjc9LBxP28E7iUgNloGgfgw406w==} + '@aws-sdk/credential-provider-web-identity@3.972.76': + resolution: {integrity: sha512-d3AGyVu759PGr35mEB2s22xxlNEA5rpdxtSPJthfPFJvoQ8dt357iVPECqWfUxXp1toJAvKmbtcIYVGigaGsCA==} engines: {node: '>=20.0.0'} - '@aws-sdk/lib-storage@3.1098.0': - resolution: {integrity: sha512-GjVYr/nZAlBNVToxFlhPiQkmICcoMFVrH5qEezrmDrcs144vagrDUhW8sfcN6G5fYbdIRRuJstfGyWlHvX6R2g==} + '@aws-sdk/lib-storage@3.1121.0': + resolution: {integrity: sha512-sx86QqM0/HjKNCS5eQGcaP9Q8GKg3FtCExO/6FfGyw00JNUsFJPmEKNCJIkWY7OmKS9jIOuJe/DtAgfr/+G/bQ==} engines: {node: '>=20.0.0'} peerDependencies: - '@aws-sdk/client-s3': ^3.1098.0 + '@aws-sdk/client-s3': ^3.1121.0 - '@aws-sdk/middleware-sdk-s3@3.972.69': - resolution: {integrity: sha512-ziSQA8AkB+u4K/5t/SStFbeoiNTG04reUeddCLNCAgmdRJGQ3+MJcpZOXXLdD6uyfCqSK1/R6004gRdXvHbYQA==} + '@aws-sdk/middleware-sdk-s3@3.972.75': + resolution: {integrity: sha512-wMIsNumRVKaNMKhvU/s9VrdEwE8S6gSzXp4RygFG5BEMnGkkXf8cjh8zf7cKJBpUDpqTWqwbz5isEgp9rH6Lng==} engines: {node: '>=20.0.0'} - '@aws-sdk/nested-clients@3.997.38': - resolution: {integrity: sha512-jQDs+nxnmgo4+WPOmY373HIC8jx4KyYYMYDI74FWrzX3Ba9fYsV0dSA+7PNYNkhRbbRCRLre83tQ31F1ACCHkA==} + '@aws-sdk/nested-clients@3.997.44': + resolution: {integrity: sha512-NhEgryjlBF9w38ZXqGymQV28IhkYa1mKhlbYnqIis57AYwWGVYfUPgg/qC2rLRqOUfblxx++irvju10kVTa8Vw==} engines: {node: '>=20.0.0'} - '@aws-sdk/signature-v4-multi-region@3.996.43': - resolution: {integrity: sha512-lKekx8bLBXSv4O+cslk9Zfnw2XKSkWBs3uWL5QGhH2ZAQfNS7FE0vcSSN2vD/AhxX54ZTywWxR4STThoeOXlBA==} + '@aws-sdk/signature-v4-multi-region@3.996.46': + resolution: {integrity: sha512-L+2xZTye/2T96f3lwCws0Zw6GG2JHZW9e8FpVgGBeeExSKyeoZ6CWRpBml/7DNiK/O26jrgPM9F+Ay8VkgzUWQ==} engines: {node: '>=20.0.0'} - '@aws-sdk/token-providers@3.1098.0': - resolution: {integrity: sha512-6cFAviffeqYGjrXn4FqGbWcTxB3nbpBpQXUm6MvnrWZThJaVBTHHnespHgSUh4yVLfzhipSh9H/XjzkHQd9P4g==} + '@aws-sdk/token-providers@3.1116.0': + resolution: {integrity: sha512-ygIivKqh8aHzNkucOCXHyIBgBpLPfrSI0mCqXF+vLBsPTUKqj0VSqAY0GFPe7lQl4HntjOcQ+KSyS7oUV2C54Q==} engines: {node: '>=20.0.0'} - '@aws-sdk/types@3.974.2': - resolution: {integrity: sha512-3W6IUtSxFbH6X7Wb7DzGCV5QiFQsd0g8bOfntpmDxQlzBoKWUMBu/JPQR0DwkE+Hpnxd6db1tXbOwdeHddG6cA==} + '@aws-sdk/types@3.974.5': + resolution: {integrity: sha512-LkwLL2BLbC6wNNm4JaH9mbEqBMdOZCct6VAYqhdN4U1xrWM+fUJQEfbHwQgDypapOWTRtlk25akb5afM0P8CIQ==} engines: {node: '>=20.0.0'} - '@aws-sdk/xml-builder@3.972.37': - resolution: {integrity: sha512-zKq4HQum8JwDyEuyfuI4bbiAcU0KxP6qy+9PR/IsR92IyE/DaBAikzAS50tjxip4bqIIANpCcG+Yyj6CVhXupg==} + '@aws-sdk/xml-builder@3.972.40': + resolution: {integrity: sha512-wlFmCIGUlwF4zx/kncw+bmxTQh1HeSJq4mYV/V5cZUSJadDP3kXvGW8Rn21cimj/7y9ju+47oYWXi97vF7czaA==} engines: {node: '>=20.0.0'} '@aws/lambda-invoke-store@0.3.0': @@ -381,8 +370,8 @@ packages: resolution: {integrity: sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==} engines: {node: '>=6.9.0'} - '@babel/generator@7.29.7': - resolution: {integrity: sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==} + '@babel/generator@7.29.8': + resolution: {integrity: sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==} engines: {node: '>=6.9.0'} '@babel/helper-compilation-targets@7.29.7': @@ -423,8 +412,8 @@ packages: resolution: {integrity: sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==} engines: {node: '>=6.9.0'} - '@babel/parser@7.29.7': - resolution: {integrity: sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==} + '@babel/parser@7.29.8': + resolution: {integrity: sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==} engines: {node: '>=6.0.0'} hasBin: true @@ -523,12 +512,12 @@ packages: resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==} engines: {node: '>=6.9.0'} - '@babel/traverse@7.29.7': - resolution: {integrity: sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==} + '@babel/traverse@7.29.8': + resolution: {integrity: sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==} engines: {node: '>=6.9.0'} - '@babel/types@7.29.7': - resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==} + '@babel/types@7.29.8': + resolution: {integrity: sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==} engines: {node: '>=6.9.0'} '@bcoe/v8-coverage@0.2.3': @@ -565,8 +554,8 @@ packages: '@emnapi/runtime@1.10.0': resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} - '@emnapi/runtime@1.11.2': - resolution: {integrity: sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA==} + '@emnapi/runtime@1.11.3': + resolution: {integrity: sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==} '@emnapi/wasi-threads@1.2.1': resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} @@ -577,12 +566,6 @@ packages: peerDependencies: eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 - '@eslint-community/eslint-utils@4.9.1': - resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 - '@eslint-community/regexpp@4.12.2': resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} @@ -631,309 +614,309 @@ packages: resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} engines: {node: '>=18'} - '@img/sharp-darwin-arm64@0.35.3': - resolution: {integrity: sha512-RMnFX7YQsMoh7lWfcM4NEHHymBX/rLuKNPVM84XE9ONPcaSCDgE7CHIHpSgPcO2xcRthgBy1HfNO319mwhIAkg==} + '@img/sharp-darwin-arm64@0.35.4': + resolution: {integrity: sha512-Uhfl4V4lhP2nbUVF9+hyH1+luj86f1gUFeo8ALYxFoULoU+G87D43BfeMP8XHsk9boxAnCY/bf2EHwhA7MuGsA==} engines: {node: '>=20.9.0'} cpu: [arm64] os: [darwin] - '@img/sharp-darwin-x64@0.35.3': - resolution: {integrity: sha512-Xo+5uFBtLN0BKqieTxiFzFPQAUlBbbH5iBKyRX/z1JrbnYsHTfKJnUfL8+p2TPXr1pXqao4eeL4Rl144uDpK9w==} + '@img/sharp-darwin-x64@0.35.4': + resolution: {integrity: sha512-hWniXY3bG5qKpkKrAwPe4y+VTPmf086YQAnkxWh7uA1YrlRouWGa0M0Mxj3ZjnXFkv7/TD1bTy9lGUK26vRvWw==} engines: {node: '>=20.9.0'} cpu: [x64] os: [darwin] - '@img/sharp-freebsd-wasm32@0.35.3': - resolution: {integrity: sha512-lUxcqWIj2wMQ9BrwNjngcr1gWUr5xgaGThBRqPPalIC2n67Cqj1uPh8NnA/ZhAg8hUbKl+kVHKwgUIwe6ZYPrg==} + '@img/sharp-freebsd-wasm32@0.35.4': + resolution: {integrity: sha512-lIsKw/BU+kjB4eZjxrYrZmwOJYi3Ajrv66iAlBmUPyKc3HpnloevB1g3wxGD9P/5BbQ1brBGl65VRRrCvQDEqA==} engines: {node: '>=20.9.0'} os: [freebsd] - '@img/sharp-libvips-darwin-arm64@1.3.2': - resolution: {integrity: sha512-9J6ypZFpQBj4YnePGoq/S38w6nz+vqg5WZLrLGY4YuSemdMq47GMLBPO42MzwdGwpg/agZ7xzZcFHa48xlywfg==} + '@img/sharp-libvips-darwin-arm64@1.3.3': + resolution: {integrity: sha512-suTBPTDGrI9WodccaDdwZItTSaBYASlBk1NSfElSHrUfzu3szG6lvIF58+WiFvnfzuK8ZBFS5zE00PxqxnRiPg==} cpu: [arm64] os: [darwin] - '@img/sharp-libvips-darwin-x64@1.3.2': - resolution: {integrity: sha512-m2pW1n6cns9VaubNwsZ+c3CRYjxNQWgJ5gPlnL1nbBcpkBvFm6SCFN5o0psFHI8w9n11NKhFkeEDns98tiqbEw==} + '@img/sharp-libvips-darwin-x64@1.3.3': + resolution: {integrity: sha512-FVJZ5mITMobmXIz/hPDTw0EintTW5H3WfrxwLqEqjiIihlu+hVRyGrFQ60xl0Lxn7Bt3zdpevPaQi0HEzqz9fw==} cpu: [x64] os: [darwin] - '@img/sharp-libvips-linux-arm64@1.3.2': - resolution: {integrity: sha512-dqVSFynCox4C/J8kT16V7SIFAns0IjgLwkvYT7p8LQVmJ5OS5b6tI9IGflxTeuBS//zXeFIUbwt5dwxyZ17cnA==} + '@img/sharp-libvips-linux-arm64@1.3.3': + resolution: {integrity: sha512-0DaL0A6Xu6sQSQFwe4iVCrKWU2cCTItnRsYsCdxAMm9NF6twAA9BKnoqy4hqz4+azQ0JHuA26qiUKsf1XJ/v5A==} cpu: [arm64] os: [linux] libc: [glibc] - '@img/sharp-libvips-linux-arm@1.3.2': - resolution: {integrity: sha512-1eMLzy92I4J6rmi4mAT8yC3HxOtniyGELlzGbNMLLeqe052ahFQ0h6LFq+lh5DsDIdYViIDst08abvSbcEdLXQ==} + '@img/sharp-libvips-linux-arm@1.3.3': + resolution: {integrity: sha512-3rbU4vqXXc3hY/OiXdl52xZvT0F1yEngWfvqudtPJg/KkyiaQw2DRsFrNzpmLvfavbwOq3qXn36GP8obHRULQA==} cpu: [arm] os: [linux] libc: [glibc] - '@img/sharp-libvips-linux-ppc64@1.3.2': - resolution: {integrity: sha512-3z0NHDxD6n5I9gc05U1eW1AyRm+Gznzq3naMrthPNqE6oYykcogW0l/jfpJdjYnuNl8R7yI9pNbE1XiUeyq0Aw==} + '@img/sharp-libvips-linux-ppc64@1.3.3': + resolution: {integrity: sha512-cdn1OvUBwsXhbC0zSzJnNzf5MZ/mTrobawDvNXBTxe8VtqKAm0sRuEY2Evzovb/w9JMk4TvRxqt1mekSuJz64w==} cpu: [ppc64] os: [linux] libc: [glibc] - '@img/sharp-libvips-linux-riscv64@1.3.2': - resolution: {integrity: sha512-bsb4rI+NldGOsXuej2r8OdSS8+zXDVaCWxyWrcv6kneTOlgAHtZABRzBBCwdsPiD90J4myNJuHpg6kA20ImW/w==} + '@img/sharp-libvips-linux-riscv64@1.3.3': + resolution: {integrity: sha512-HjPVx7yKz+0lqdhDlTw1tt90wamBoxhiXpvl1XZpJLiHH4RCJ5yDTqH+VlYPv2fwFs89JFw4c1IexYOcQUi4IQ==} cpu: [riscv64] os: [linux] libc: [glibc] - '@img/sharp-libvips-linux-s390x@1.3.2': - resolution: {integrity: sha512-/ABshyj8gCpyIrNXnHn4LorDJ0HHm1VhXPBlxZ8zAtfVPAaSafXPGn+sUSIRiwaSBy0mmFjSjiXI5mkcwdChKQ==} + '@img/sharp-libvips-linux-s390x@1.3.3': + resolution: {integrity: sha512-neWLh+3yCNThxnfy3c4BbVBeGgt9aftno+XbT56iK28RgeDs3UOFWviLWlUu0bArYVYJaFDK+RRohbicUNCm8Q==} cpu: [s390x] os: [linux] libc: [glibc] - '@img/sharp-libvips-linux-x64@1.3.2': - resolution: {integrity: sha512-ITPEtgffGJ0S6G9dRyw/366tJQqFRcHWPHhC+Stpg3Z8AEMrDrTr2lhdz4f/Y/HMbRh//7Z5mBzEpVdi62Oc3w==} + '@img/sharp-libvips-linux-x64@1.3.3': + resolution: {integrity: sha512-4vKmvAst9nrowcqquKFAyZJUDolUaIp8uRiN0mWFguJ1IplC9/pitXtlnnlU4aa/eJw3J7i67V+pwUL+wZGdsA==} cpu: [x64] os: [linux] libc: [glibc] - '@img/sharp-libvips-linuxmusl-arm64@1.3.2': - resolution: {integrity: sha512-zE9EdiUzUmg5mDT5a1rk5fYJ6GWPloTwWBYDS14naqHsL+EaMpDj1AWnpLgh3u0YCORv2Tt50wrcrpYqkP97Kw==} + '@img/sharp-libvips-linuxmusl-arm64@1.3.3': + resolution: {integrity: sha512-Y9kQaLMuNoB0bPYOOdcZMaseNrFpPodIWWMrx+CZyydf2xn68j9WYc6sWWRrDwNkzCQjKYfc68L7jKjGlHMibw==} cpu: [arm64] os: [linux] libc: [musl] - '@img/sharp-libvips-linuxmusl-x64@1.3.2': - resolution: {integrity: sha512-m0lrLiUt+lBYnCFr8qV/65yMR4E/c7/wf78I5eKTdkEakFAlZ9QlzEM3QIhhAwVeUhLAHLcCq7a7Vszq/oFNZQ==} + '@img/sharp-libvips-linuxmusl-x64@1.3.3': + resolution: {integrity: sha512-fj8Mv0HHfD1Rr+4I68+3agJynxDWtBFgicTbSOb9Bke6pIwzGcJ+RX/yHjmiEGFMCavY/dxvem7MyNaJF+wDiw==} cpu: [x64] os: [linux] libc: [musl] - '@img/sharp-linux-arm64@0.35.3': - resolution: {integrity: sha512-QgKDspHPnrU+GQ55XPhGwyhC8acLVOOSyAvo1oVfFmrIXLkDNmGWzAfDZ4xK8oSA1qBQrALcHX0G5UZni/SuFQ==} + '@img/sharp-linux-arm64@0.35.4': + resolution: {integrity: sha512-De4jpEnAU8Hd5oT0j1G3uL4ZvTuipVMn7YC6vPaJhy6/7EwEae0SVAoBrUMYQbkLGDm85taVWwuPc1a44LTzCQ==} engines: {node: '>=20.9.0'} cpu: [arm64] os: [linux] libc: [glibc] - '@img/sharp-linux-arm@0.35.3': - resolution: {integrity: sha512-affVWCTLooy8TSxbDx2qkzuDeaWLNVBA+P//FNBirHsXpP2fuBhk5AuboYUnrDnzoXes8GFjpTx0SBFOCRg+FA==} + '@img/sharp-linux-arm@0.35.4': + resolution: {integrity: sha512-7OAS8gI0EReKGVN2HssHlM6umJgxF5VI3xN0p9FA91p/YO+ou5hiNghLdZ5BEHztwaaK5+bLKRf8x/o2L2nk9A==} engines: {node: '>=20.9.0'} cpu: [arm] os: [linux] libc: [glibc] - '@img/sharp-linux-ppc64@0.35.3': - resolution: {integrity: sha512-sMd8rDxmpLOwv/7N44klFjOD5DUO7FLdjiXDI0hoxYaf7Ar262dQIEkosE98bps+5HPLtp/EvNqeqQtOycP/IA==} + '@img/sharp-linux-ppc64@0.35.4': + resolution: {integrity: sha512-2oYZJeIl4kCcMGk4ouZVjnkCtFrpQFlNEtJ6GbxzhHQchwH0NH/qEb9ykmOl29dqwMq+JhFdZn+1ak2FKhI9fQ==} engines: {node: '>=20.9.0'} cpu: [ppc64] os: [linux] libc: [glibc] - '@img/sharp-linux-riscv64@0.35.3': - resolution: {integrity: sha512-0Eob78yjlYPfL5vMNWAW55l3R9Y6BQS/gOfe0ZcP9mEz9ohhKSt4im1hayiknXgf8AWrFqMvJcKIdmLmEe7yeQ==} + '@img/sharp-linux-riscv64@0.35.4': + resolution: {integrity: sha512-cPbNChoRURAWdebDIHSenxRpgEdy7JkPydSnUxRm9VvKD7m0/xVaR/8Fzlu81pk5nHEvHH87UZUA7cTtwnbJSA==} engines: {node: '>=20.9.0'} cpu: [riscv64] os: [linux] libc: [glibc] - '@img/sharp-linux-s390x@0.35.3': - resolution: {integrity: sha512-KgAxQ0DxpNOq1rG2t5cgTgShJFGSuU7XO45cqC+1NVOuZnP6tlgZRuSYOfNupGkHID0o3cJOsw4DVeJpMovcGw==} + '@img/sharp-linux-s390x@0.35.4': + resolution: {integrity: sha512-RY0JFY8Fd6RonCBtHz+DvadaPkXDSI1AUn6yWL9TipqkZ1vY8w8evqdgyDFnkm4/K1ve1TvZiaePP5oSd4+WVQ==} engines: {node: '>=20.9.0'} cpu: [s390x] os: [linux] libc: [glibc] - '@img/sharp-linux-x64@0.35.3': - resolution: {integrity: sha512-8pqvxubL2PGdhlPy6GLqzDYMUjyRmKAwKHYKixpdJYBUK7PJ0C029XdsnpFIdgRZG68fZiGdHVWcKPvtiPB4cA==} + '@img/sharp-linux-x64@0.35.4': + resolution: {integrity: sha512-9qvvEAuk8k89TfWUoX2htWjbAMX8p+NxCppjpcg5k6xMsjhBQPTsoIh36h9Qde4WRuGpJeYnOjdosDn/cnv+OA==} engines: {node: '>=20.9.0'} cpu: [x64] os: [linux] libc: [glibc] - '@img/sharp-linuxmusl-arm64@0.35.3': - resolution: {integrity: sha512-Vz0iQjzzcSX3HCbfwFfCSG/9SCIqyO0mH2sXyiHaAYfBk0cRsCWXRyQYX0ovCK/PAQBbTzQ0dsPQHh5MAFL59w==} + '@img/sharp-linuxmusl-arm64@0.35.4': + resolution: {integrity: sha512-KB5jxpfWQTr0nc3xdHtWChdbifHrBGsd2SM62Eyxrl8afikm+f5qGBU75SJIZBT/S1MC8XyacdlXBMSWq6OURA==} engines: {node: '>=20.9.0'} cpu: [arm64] os: [linux] libc: [musl] - '@img/sharp-linuxmusl-x64@0.35.3': - resolution: {integrity: sha512-6O1NPKcDVj9QEdg7Hx549EX8U0rp6yXQERqru6yRN7fGBn32UvIRJUlWnk+8xDCiG76hXVBbX82NZ/ZKr0euIg==} + '@img/sharp-linuxmusl-x64@0.35.4': + resolution: {integrity: sha512-f+eZJZIQNEEd26RPSW+76chwOf1XtA2Y/O+5ocVyLliHkeih3e+jhLVBdNTd2rS3IbNXK8+ug93Vf5ZXtF5Lxg==} engines: {node: '>=20.9.0'} cpu: [x64] os: [linux] libc: [musl] - '@img/sharp-wasm32@0.35.3': - resolution: {integrity: sha512-cZ0XkcYGpHZkqW6iCkqTcmUC0CD9DhD5d/qeZlZkfRBn6GnHniZXLUo5+9xw8Iv76YE6LQFN9YNBlKREcCG76w==} + '@img/sharp-wasm32@0.35.4': + resolution: {integrity: sha512-zQnl4Kwp7Q6NHsENtU2T/00Zi+w3AQNwz3+UaTyVBy2FpXrzXzGjndpK61onhZjRtRpQXxCTeqw19bVyXOh7jA==} engines: {node: '>=20.9.0'} - '@img/sharp-webcontainers-wasm32@0.35.3': - resolution: {integrity: sha512-2rnq7bX3NzeR2T4YWgz8qiG4h3TSdMe+vN1iQXpJleSJ3SM5zQ8Fy2SyyXAWlbxpEZ2Y+Z4u1BePgJEYbSy80Q==} + '@img/sharp-webcontainers-wasm32@0.35.4': + resolution: {integrity: sha512-ESfNkywmCfPNyaZjxooddJQiQ+l/nTpGEOGthxiLnIHXC/CmcBixnfwUleX9mCz9ovrUUvKMap/pm8RYbzfwaA==} engines: {node: '>=20.9.0'} cpu: [wasm32] - '@img/sharp-win32-arm64@0.35.3': - resolution: {integrity: sha512-4bPwFdMbeC4JQ8L8LOyWp6nsHcboP5fxkp6iPOXz2Vg49R42TuMs2whkJ5OAP4/Ul035qOzy0AecOF9VOscn4w==} + '@img/sharp-win32-arm64@0.35.4': + resolution: {integrity: sha512-iNdlBX9gLVvqe2I3uIJSIKTq6wckP/DYxZtcqxm09x5Gi24DnFBmPAWZmr60ZyYMG0xlzo6goG3670ar+RXvRw==} engines: {node: '>=20.9.0'} cpu: [arm64] os: [win32] - '@img/sharp-win32-ia32@0.35.3': - resolution: {integrity: sha512-r53mXsBN6lFUDiST764SvgwUdHAqM4rPAiDzAmf4fLoB6X/rkfyTrLCg6+g17wJJiCmB3JYgHuUldCWUIRFSXw==} + '@img/sharp-win32-ia32@0.35.4': + resolution: {integrity: sha512-kqRsbaa5CS6KHlpxnN7WhE6vAAugXyZButpRdvDWetlv6Qv4N9WTcrWzF7tXfB9T7MsoadqdI8hmwLq6UlLvtw==} engines: {node: ^20.9.0} cpu: [ia32] os: [win32] - '@img/sharp-win32-x64@0.35.3': - resolution: {integrity: sha512-D4y1vNeZrIIJCN+uHaWVtH86B+aCrdMYYjicy9pXHvbGZeGYLLSd3wdVuC37FxVXlU1ARsk84eKWfWMXGYEqvA==} + '@img/sharp-win32-x64@0.35.4': + resolution: {integrity: sha512-XtmnYhBcrORsJ4XJngyzr/EWP0hRZLAZRFaApdKuviyqF78+ylxh2y06ZmtULAMOnObJ3ucpN0AcwSWnMowTRg==} engines: {node: '>=20.9.0'} cpu: [x64] os: [win32] - '@inquirer/ansi@1.0.2': - resolution: {integrity: sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==} - engines: {node: '>=18'} + '@inquirer/ansi@2.0.7': + resolution: {integrity: sha512-3eTuUO1vH2cZm2ZKHeQxnOqlTi9EfZDGgIe3BL3I4u+rJHocr9Fz86M4fjYABPvFnQG/gGK551HqDiIcETwU6Q==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} - '@inquirer/checkbox@4.3.2': - resolution: {integrity: sha512-VXukHf0RR1doGe6Sm4F0Em7SWYLTHSsbGfJdS9Ja2bX5/D5uwVOEjr07cncLROdBvmnvCATYEWlHqYmXv2IlQA==} - engines: {node: '>=18'} + '@inquirer/checkbox@5.2.3': + resolution: {integrity: sha512-XEYX2WA8SBkLPczL6/yXPHLPCvDoptmh9v56Cy05BSV1Smk1vWy19bTC4qJBuIffw7+6l4CcaYYzGqG60RfW1g==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} peerDependencies: '@types/node': '>=18' peerDependenciesMeta: '@types/node': optional: true - '@inquirer/confirm@5.1.21': - resolution: {integrity: sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==} - engines: {node: '>=18'} + '@inquirer/confirm@6.3.0': + resolution: {integrity: sha512-pZHXJImFtERmSNMBHcjwuz8Ck5vEFEYNUZnwbb8aJpjHv/TwGuFErNxF2Hp8+V+pNJs2EYPMlyWscvFEqO9jOQ==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} peerDependencies: '@types/node': '>=18' peerDependenciesMeta: '@types/node': optional: true - '@inquirer/core@10.3.2': - resolution: {integrity: sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==} - engines: {node: '>=18'} + '@inquirer/core@12.0.1': + resolution: {integrity: sha512-JMD5Jy/ScL5TZE18m83Nw25HjqGFLoWXwnEkW7IdwwAhZpB9Bus55/WU7zn3UqR1MOCjjTQOIYpiD4vjWA3LPw==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} peerDependencies: '@types/node': '>=18' peerDependenciesMeta: '@types/node': optional: true - '@inquirer/editor@4.2.23': - resolution: {integrity: sha512-aLSROkEwirotxZ1pBaP8tugXRFCxW94gwrQLxXfrZsKkfjOYC1aRvAZuhpJOb5cu4IBTJdsCigUlf2iCOu4ZDQ==} - engines: {node: '>=18'} + '@inquirer/editor@5.3.1': + resolution: {integrity: sha512-y43COoyVUjPWIobn2Qep/uI1drPS78aaZZZ9kVi94Tyu/GuW2N8d8Q4rifJXGAXCEAXCPTTMjD8gC1HyvM5ukA==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} peerDependencies: '@types/node': '>=18' peerDependenciesMeta: '@types/node': optional: true - '@inquirer/expand@4.0.23': - resolution: {integrity: sha512-nRzdOyFYnpeYTTR2qFwEVmIWypzdAx/sIkCMeTNTcflFOovfqUk+HcFhQQVBftAh9gmGrpFj6QcGEqrDMDOiew==} - engines: {node: '>=18'} + '@inquirer/expand@5.1.3': + resolution: {integrity: sha512-3NQJiXNJ/aj9wiAsr7pECdp5Qe9J0X9YUJCKsaFXS+ddOxfL6J4AIl3w3T4Gq3kK0WsQY5GMoDokK5X94m6lHw==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} peerDependencies: '@types/node': '>=18' peerDependenciesMeta: '@types/node': optional: true - '@inquirer/external-editor@1.0.3': - resolution: {integrity: sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==} - engines: {node: '>=18'} + '@inquirer/external-editor@3.0.4': + resolution: {integrity: sha512-tZbbaK2ovq6vlrRBNQvjrypmrED/p5x2ncIHQ79cD55tei3dD96v5glMMA+6tiq7K104i/25DVYKWVPJuV6ptA==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} peerDependencies: '@types/node': '>=18' peerDependenciesMeta: '@types/node': optional: true - '@inquirer/figures@1.0.15': - resolution: {integrity: sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==} - engines: {node: '>=18'} + '@inquirer/figures@2.0.8': + resolution: {integrity: sha512-tApbon79GM9ry56ja/Ud3SY2CL4TQsao9fIwDQbgTeNY55025GdMzQ2+UdegV/lx51VNGUB59M0v0nMpybYY4Q==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} - '@inquirer/input@4.3.1': - resolution: {integrity: sha512-kN0pAM4yPrLjJ1XJBjDxyfDduXOuQHrBB8aLDMueuwUGn+vNpF7Gq7TvyVxx8u4SHlFFj4trmj+a2cbpG4Jn1g==} - engines: {node: '>=18'} + '@inquirer/input@5.1.4': + resolution: {integrity: sha512-3xQkQrOvgOzpSN2ciTVdRDlg1FWMCA8l+0KfB6SNlILoTCGzJTzO/gc0Rwjcb3usuGyKdaGtI6OiyMdeMeLWkg==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} peerDependencies: '@types/node': '>=18' peerDependenciesMeta: '@types/node': optional: true - '@inquirer/number@3.0.23': - resolution: {integrity: sha512-5Smv0OK7K0KUzUfYUXDXQc9jrf8OHo4ktlEayFlelCjwMXz0299Y8OrI+lj7i4gCBY15UObk76q0QtxjzFcFcg==} - engines: {node: '>=18'} + '@inquirer/number@4.2.1': + resolution: {integrity: sha512-5KaqwZNLRpUuWcoCrYghPP9TMaXL5v2Sk4xqePM7RCVegcJStoXdWibio60YIC1bec+z1fCyb67N6XPJIkZtGA==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} peerDependencies: '@types/node': '>=18' peerDependenciesMeta: '@types/node': optional: true - '@inquirer/password@4.0.23': - resolution: {integrity: sha512-zREJHjhT5vJBMZX/IUbyI9zVtVfOLiTO66MrF/3GFZYZ7T4YILW5MSkEYHceSii/KtRk+4i3RE7E1CUXA2jHcA==} - engines: {node: '>=18'} + '@inquirer/password@5.2.0': + resolution: {integrity: sha512-CvVcW09emkBESEOW+4R8CjLNkP3fB3XrjeL8CDvfpjgrJN+V9oerXmJAXXM3l+4xqYPD5Yaujzy/Ph0PLOdDuA==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} peerDependencies: '@types/node': '>=18' peerDependenciesMeta: '@types/node': optional: true - '@inquirer/prompts@7.10.1': - resolution: {integrity: sha512-Dx/y9bCQcXLI5ooQ5KyvA4FTgeo2jYj/7plWfV5Ak5wDPKQZgudKez2ixyfz7tKXzcJciTxqLeK7R9HItwiByg==} - engines: {node: '>=18'} + '@inquirer/prompts@8.5.2': + resolution: {integrity: sha512-IYR/3C/paEVVQYQvdDlFZVjRCJVYHHON0XXMH91KO9GSxs0TdKYWlUdvfQl2EfAHDxUaN3IBffkE/BDTh5nJ6g==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} peerDependencies: '@types/node': '>=18' peerDependenciesMeta: '@types/node': optional: true - '@inquirer/prompts@7.3.2': - resolution: {integrity: sha512-G1ytyOoHh5BphmEBxSwALin3n1KGNYB6yImbICcRQdzXfOGbuJ9Jske/Of5Sebk339NSGGNfUshnzK8YWkTPsQ==} - engines: {node: '>=18'} + '@inquirer/prompts@8.7.0': + resolution: {integrity: sha512-yQwBMYvpJ6jqrXtKiOwRD5XezjJoyt3VQvIyjsr5Arqb519nfIohOQymWVJ8/vEgg8xtZerrCsqlSaqt/LPC9A==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} peerDependencies: '@types/node': '>=18' peerDependenciesMeta: '@types/node': optional: true - '@inquirer/rawlist@4.1.11': - resolution: {integrity: sha512-+LLQB8XGr3I5LZN/GuAHo+GpDJegQwuPARLChlMICNdwW7OwV2izlCSCxN6cqpL0sMXmbKbFcItJgdQq5EBXTw==} - engines: {node: '>=18'} + '@inquirer/rawlist@5.3.3': + resolution: {integrity: sha512-Mu7WrtmDLaXBDEyrRLS70SZgX9ZSm4Up1w0ZxiH8C1OOp9oaVCn2k8q3QGgmlnhsKYUhuaU3zFWhAP6wxkVIMA==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} peerDependencies: '@types/node': '>=18' peerDependenciesMeta: '@types/node': optional: true - '@inquirer/search@3.2.2': - resolution: {integrity: sha512-p2bvRfENXCZdWF/U2BXvnSI9h+tuA8iNqtUKb9UWbmLYCRQxd8WkvwWvYn+3NgYaNwdUkHytJMGG4MMLucI1kA==} - engines: {node: '>=18'} + '@inquirer/search@4.3.1': + resolution: {integrity: sha512-0VWOvsHWI0rPj6CG70MoP4oXNCB6adcyN8bVFZXnh11eLDdPIK2f2XCmva78acPPDfJisfab7qNakwBh7hdBXw==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} peerDependencies: '@types/node': '>=18' peerDependenciesMeta: '@types/node': optional: true - '@inquirer/select@4.4.2': - resolution: {integrity: sha512-l4xMuJo55MAe+N7Qr4rX90vypFwCajSakx59qe/tMaC1aEHWLyw68wF4o0A4SLAY4E0nd+Vt+EyskeDIqu1M6w==} - engines: {node: '>=18'} + '@inquirer/select@5.2.3': + resolution: {integrity: sha512-KuRTodDa6xBXX2noIpjuitpX/QT7Sfav7dIZ/OfUY54Hxg95nrGoshSzxx6Ey7qqLbImKdiGkSDt7KjPXjQgmA==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} peerDependencies: '@types/node': '>=18' peerDependenciesMeta: '@types/node': optional: true - '@inquirer/type@3.0.10': - resolution: {integrity: sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==} - engines: {node: '>=18'} + '@inquirer/type@4.1.0': + resolution: {integrity: sha512-FMiJpuHUG3Dk0ex+UIXkre7i+i4OcwHWk9YdcVtZHFwb/r2rnrU2ipTCNAB7A+QOP0ryzIcqOfy76fRyyvOEAw==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} peerDependencies: '@types/node': '>=18' peerDependenciesMeta: '@types/node': optional: true - '@ioredis/commands@1.10.0': - resolution: {integrity: sha512-UmeW7z4LfctwoQ5wkhVzgq8tXkreED2xZGpX+Bg+zA+WJFZCT6c062AfCK/Dfk81xZnnwdhJCUMkitihRaoC2Q==} + '@ioredis/commands@2.0.0': + resolution: {integrity: sha512-vrx0AE/T0h7cRZwfo1M39Cr+ZhZrkf0V8mQN75wucKCxCLD9l/VX6no3gFvrLqD1IlG/1LtzWovqEw3t0Vr9zg==} '@isaacs/cliui@8.0.2': resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} @@ -947,12 +930,12 @@ packages: resolution: {integrity: sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==} engines: {node: '>=8'} - '@jest/console@30.4.1': - resolution: {integrity: sha512-v3bhyxUh9Hgmo5p6hAOXe14/R3ZxZDOsvHleh4B07z3m/x4/ngPUXEm9XwK4sF4u+f+P2ORb0Ge+MgpaqRMVDA==} + '@jest/console@30.5.0': + resolution: {integrity: sha512-BI1DpOedrJqbrYVi9yNhDWGjqphR/+gsM4STmg2+VaeXm7851hvpBDyKOZGMXATTrGEnFuEseQvLCtrrRmG0GQ==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - '@jest/core@30.4.2': - resolution: {integrity: sha512-TZJA6cPJUFxoWhxaLo8t0VX/MZX2wPWr0uIDvLSHIvN4gu9h02vSzqI2kBADG1ExqQlC+cY09xKMSreivvrChQ==} + '@jest/core@30.5.0': + resolution: {integrity: sha512-DLjRME+NY//j+UDTSfWnjoP0srrdR3DrJRy7yFZktGIwzpN2iVy2vMo0jziZ5c2Ij7bOwlJRXKVWtxZusazOJg==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} peerDependencies: node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 @@ -960,40 +943,40 @@ packages: node-notifier: optional: true - '@jest/diff-sequences@30.4.0': - resolution: {integrity: sha512-zOpzlfUs45l6u7jm39qr87JCHUDsaeCtvL+kQe/Vn9jSnRB4/5IPXISm0h9I1vZW/o00Kn4UTJ2MOlhnUGwv3g==} + '@jest/diff-sequences@30.5.0': + resolution: {integrity: sha512-OsqBjHXCn8cadasoAZBP6nWYvMsRhpMzGXTpxJ5aO04NlbdhIz+FVe3q49l0AwVhsz/cEmIpBes6gAFl1/dWQg==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - '@jest/environment@30.4.1': - resolution: {integrity: sha512-AK9yNRqgKxiabqMoe4oW+3/TSSeV8vkdC7BGaxZdU0AFXfOpofTLqdru2GXKZghP3sdgwE9XXpnVwfZ8JnFV4w==} + '@jest/environment@30.5.0': + resolution: {integrity: sha512-HUaqexIauIh69IQ4NTuPDEUCB8g8T4TOPSIzQOS18mwI/KEHKQk1j013K2o6ra031szZE2t5jGmVx3xbzdjgKA==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - '@jest/expect-utils@30.4.1': - resolution: {integrity: sha512-ZBn5CglH8fBsQsvs4VWNzD4aWfUYks+IdOOQU3MEK71ol/BcVm+P+rtb1KpiFBpSWSCE27uOahyyf1vfqOVbcQ==} + '@jest/expect-utils@30.5.0': + resolution: {integrity: sha512-5j0ztPxSy3McUJihjkDdCyCfjvT2hxykFTWsgEBZKB8qsw9ALdCiGTpTRH5gnf/d+qI4SflYUJ0dWNbzjQCWbA==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - '@jest/expect@30.4.1': - resolution: {integrity: sha512-ginrj6TMgh2GshLUGCjO94Ptx9HhdZA/I6A9iUfyeLKFtdAjnKzHDgzgP9HYQgbxM1lbXScQ2eUBz2lGeVDPWA==} + '@jest/expect@30.5.0': + resolution: {integrity: sha512-jEmgmgJEobJ3zEhDOGp1VAJ6JkoVelpS8uZ1ae1Ul/5lP78UKJKmmU0lciJwd6JdnqOXHaaS/QCKwbf1dHI9MA==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - '@jest/fake-timers@30.4.1': - resolution: {integrity: sha512-iW5umdmfPeWzehrVhugFQZqCchSCud5S1l2YT0O9ZhjRR0ExclANDZkiSBwzqtnlOn0J1JXvO+HZ6rkuyOVOgQ==} + '@jest/fake-timers@30.5.0': + resolution: {integrity: sha512-sg8xIbYwe5GdB/vT3/0qrDIpO7Ov9mazHi++M95uynmDKEZ70G1r169AWct73H07VrTZhrz1SJEfLtjYv8tE3A==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - '@jest/get-type@30.1.0': - resolution: {integrity: sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==} + '@jest/get-type@30.5.0': + resolution: {integrity: sha512-9/2VUPitAjmBzbvDvqrxmvB7BzWsBW0WmkkojX1ODuxX1NLGxx9gfaZpHB0z8DtJ9uhGNmZG/VXBhf8uO0OV8Q==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - '@jest/globals@30.4.1': - resolution: {integrity: sha512-ZbuY4cmXC8DkxYjfvT2DbcHWL2T6vmsMhXCDcmTB2T0y0gaezBI77ufq5ZAIdcRkYZ7NEQEDg1xFeKbxUJ5v5Q==} + '@jest/globals@30.5.0': + resolution: {integrity: sha512-h7eJx534czwL8lQMYB0hwLT4/HquO8EX/RtYL7RNUHyUyWWVciYjoudN4Ns5JmNvn2/jh0Vm9UstZjEzJJ5EsQ==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - '@jest/pattern@30.4.0': - resolution: {integrity: sha512-RAWn3+f9u8BsHijKJ71uHcFp6vmyEt6VvoWXkl6hKF3qVIuWNmudVjg12DlBPGup/frIl5UcUlH5HfEuvHpEXg==} + '@jest/pattern@30.5.0': + resolution: {integrity: sha512-HdNQYSdRTEBNrginaqzQtTjG0HRMfrra/z6Ok7uL3S87vSlarIVohEsJsSj5edu3MiHoHjAkvPROz5ZjoKai+w==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - '@jest/reporters@30.4.1': - resolution: {integrity: sha512-/SnkPCzEQpUaBH81kjdEdDdo2WZl5hxw+BmLDGWjRkm8o7XlhjwsU36cqwe5PGBE5WYpBvDzRSdXx9rbGuJtNA==} + '@jest/reporters@30.5.0': + resolution: {integrity: sha512-FEAuusWm+PUOn9ydjaHhpOpyPmS6IbGE04HaKTuUI4zd8eqYZiXiNLoCsdCog/l2XnNj53E9pFy64UltcvfJKg==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} peerDependencies: node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 @@ -1001,32 +984,32 @@ packages: node-notifier: optional: true - '@jest/schemas@30.4.1': - resolution: {integrity: sha512-i6b4qw5qnP8c5FEeBJg/uZQ4ddrkN6Ca8qISJh0pr7a5hfn3h3v5x60BEbOC7OYAGZNMs1LfFLwnW2CuK8F57Q==} + '@jest/schemas@30.5.0': + resolution: {integrity: sha512-/hunigyNpc4RCjC0VaW3f5RCUZVM2+WQ65qP7z083Gmvac7or2LI50XVNOtE4YPgBpV0yxYiAgorAPGniCoJmg==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - '@jest/snapshot-utils@30.4.1': - resolution: {integrity: sha512-ObY4ljvQ95mt6iwKtVLetR/4yXiAgl3H4nJxhztr0MTjrN97TwDYrnCp/kF60Ec9HdhkWTHSu+Hg05aXfngpOA==} + '@jest/snapshot-utils@30.5.0': + resolution: {integrity: sha512-iWQtIsi2dRsO2oWzVceOeynuRJiYTW8gsDVp5wFQ02ipHluQsNgBpasWSHiawVxukIlsGLdCtCSbIEK7fPtpvQ==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - '@jest/source-map@30.0.1': - resolution: {integrity: sha512-MIRWMUUR3sdbP36oyNyhbThLHyJ2eEDClPCiHVbrYAe5g3CHRArIVpBw7cdSB5fr+ofSfIb2Tnsw8iEHL0PYQg==} + '@jest/source-map@30.5.0': + resolution: {integrity: sha512-xWpTJP9D0bDFGbPGT8XuWSwwha/iHADyyKzUnMx4UbdgnHugxrDaQFO4RZ8x4ZsFzRP6pNii8uvlgKCDxCIuDg==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - '@jest/test-result@30.4.1': - resolution: {integrity: sha512-/ZG7pgEiOmmWkN9TplKbOu4id2N5lh7FHwRwlkgBVAzGdRH+OkkQ8wX/kIxg4zmd3ZQvAL1RwL2yWsvNYYECTw==} + '@jest/test-result@30.5.0': + resolution: {integrity: sha512-9IlPqUzUMkVDmoDqSSrVVLroVotgN3hTUPWPwq24XWXhh1Zpg915RXZ5pgRJ/7j4YE/kng5nqjSn4p3S68SZJA==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - '@jest/test-sequencer@30.4.1': - resolution: {integrity: sha512-PeYE+4td5rKjoRPxztObrXU+H8hsjZfxKMXOcmrr34JerSyB/ROOxbbicz8B7A5j9R9VayDnVPvBmedqCsFCdw==} + '@jest/test-sequencer@30.5.0': + resolution: {integrity: sha512-TXlvSDIVv482b83hD8A8WtxDJEmGF47f60W6jRXOQL0Isfs3hKtk4rZIm9R/jLzAibg8+c56y+Q00BQs8R7Xcg==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - '@jest/transform@30.4.1': - resolution: {integrity: sha512-Wz0LyktlTvRefoymh+n64hQ84KNXsRGcwdoZ8CSa0Ea+fgYcHZlnk+hDP7v2MS7il2bQ5uTEIxf4/NNfhMN4KQ==} + '@jest/transform@30.5.0': + resolution: {integrity: sha512-n1cYhoByyULEIXi64wbT4Lq91qeT1E6bwpM//sprFXhw955qaiHTdAmy1c1rNFGB6fCf1J+nxDUSf3RGwgZP5A==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - '@jest/types@30.4.1': - resolution: {integrity: sha512-f1x/vJXIfjOlEmejYpbkbgw1gOqpPECwMvMEtBqe47j7H2Hg8h8w3o3ikhSXq3MI15kg+oQ0exWO0uCtTNJLoQ==} + '@jest/types@30.5.0': + resolution: {integrity: sha512-s1N+79S4Yp9ZgklCauZXi+YPJdCdtStNYQT32stuD6EeQaIBGHoUfyj2P0YWy8RmuQfaJboO+ulxEvEheR/POQ==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} '@jridgewell/gen-mapping@0.3.13': @@ -1042,8 +1025,8 @@ packages: '@jridgewell/source-map@0.3.11': resolution: {integrity: sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==} - '@jridgewell/sourcemap-codec@1.5.5': - resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + '@jridgewell/sourcemap-codec@1.6.0': + resolution: {integrity: sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==} '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} @@ -1058,27 +1041,46 @@ packages: '@microsoft/tsdoc@0.16.0': resolution: {integrity: sha512-xgAyonlVVS+q7Vc7qLW0UrJU7rSFcETRWsqdXZtjzRU8dF+6CkozTK4V4y1LwOX7j8r/vHphjDeMeGI4tNGeGA==} - '@napi-rs/wasm-runtime@1.1.6': - resolution: {integrity: sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==} + '@napi-rs/wasm-runtime@1.2.3': + resolution: {integrity: sha512-UMduMbqO5s5zF2NkNacMT/yK5Y5QiKvWr2+50bzIIxFDwVJ2h49b+oyjaCGPhJxd2/gC2x39EHv/gHVuu36x2Q==} + engines: {node: ^20.19.0 || ^22.13.0 || >=23.5.0} peerDependencies: - '@emnapi/core': ^1.7.1 - '@emnapi/runtime': ^1.7.1 + '@emnapi/core': ^1.7.1 || ^2.0.0-alpha.4 + '@emnapi/runtime': ^1.7.1 || ^2.0.0-alpha.4 - '@nestjs/cli@11.0.24': - resolution: {integrity: sha512-aIHxQLSYtXShifA3zwWIeznEsZnNa3Iz2QRykFj+sl9IcbERBHr5nH87FRgywM+He3NxoF5WazHfR8FsmVeWxw==} + '@nestjs/cli@12.0.0': + resolution: {integrity: sha512-DxgGUKA4gj92lU2lF23Mlvm6y7AQBKEBuanNKNcmHeP6Cn3yqPZxN0LqyaJys25JCqO1bN8KgE3LICrvYlB93g==} engines: {node: '>= 20.11'} hasBin: true peerDependencies: - '@swc/cli': ^0.1.62 || ^0.3.0 || ^0.4.0 || ^0.5.0 || ^0.6.0 || ^0.7.0 || ^0.8.0 - '@swc/core': ^1.3.62 + '@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 - '@nestjs/common@11.1.28': - resolution: {integrity: sha512-bRImsxibie+AM7xjdwcrm/gr5YeacI65kSBNzTufa1Ib5iwziaY/lqMtRh9THq6pbV4e1HP9aI2ZxGUumnmaoQ==} + '@nestjs/common@12.0.1': + resolution: {integrity: sha512-v0zTaRCTV2K2xSnb3GnJoQKtaR6VxouJtm+P2gpr5w61dlXeWpXl1WVknCSzUqx2QwTV10tBkHETxvQvkf2Exg==} peerDependencies: class-transformer: '>=0.4.1' class-validator: '>=0.13.2' @@ -1090,20 +1092,20 @@ packages: class-validator: optional: true - '@nestjs/config@4.0.4': - resolution: {integrity: sha512-CJPjNitr0bAufSEnRe2N+JbnVmMmDoo6hvKCPzXgZoGwJSmp/dZPk9f/RMbuD/+Q1ZJPjwsRpq0vxna++Knwow==} + '@nestjs/config@12.0.0': + resolution: {integrity: sha512-pG5+fFfCo3cdgVkLJLvvhUGfxEjwzv9hIAQ1aJVHvHlHVniDWfwX5pR/utgoAkJzSikv2oTR1BmIF03FXdi42Q==} peerDependencies: - '@nestjs/common': ^10.0.0 || ^11.0.0 + '@nestjs/common': ^11.0.0 || ^12.0.0 rxjs: ^7.1.0 - '@nestjs/core@11.1.28': - resolution: {integrity: sha512-06m63xIRj8+l8uOeh/8LnYupGubkyu4f+bPKIadaSui6vK9KpXgoz7HveT1yOVLcEt0M0oCOEW5EuEXZkEmBBQ==} + '@nestjs/core@12.0.1': + resolution: {integrity: sha512-rU6tAi8vDdyzHgN0iW0J4UJvziroOqzHqzlqL7phOSPizaXVEePfv+OUOsdJEOh0Qd14mslc3udOheLrAEcZow==} engines: {node: '>= 20'} peerDependencies: - '@nestjs/common': ^11.0.0 - '@nestjs/microservices': ^11.0.0 - '@nestjs/platform-express': ^11.0.0 - '@nestjs/websockets': ^11.0.0 + '@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: @@ -1114,15 +1116,16 @@ packages: '@nestjs/websockets': optional: true - '@nestjs/jwt@11.0.2': - resolution: {integrity: sha512-rK8aE/3/Ma45gAWfCksAXUNbOoSOUudU0Kn3rT39htPF7wsYXtKfjALKeKKJbFrIWbLjsbqfXX5bIJNvgBugGA==} + '@nestjs/jwt@12.0.1': + resolution: {integrity: sha512-AOvjo0JIuVMNr+6JgbEz4fP+4afclGR5ffZnHtjOFs4+HhzerUYKcIVUL06u5uQYQi/ra8WHO+NZR7OFdQeGvA==} peerDependencies: - '@nestjs/common': ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 + '@nestjs/common': ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 || ^12.0.0 - '@nestjs/mapped-types@2.1.1': - resolution: {integrity: sha512-SCCoMEJ6jdeI5h/N+KCVF1+pmg/hmEkNA5nHTS8Gvww7T/LCl4o1gFLinw2iQ60w7slFkszHcGLKGdazVI4F8A==} + '@nestjs/mapped-types@12.0.0': + resolution: {integrity: sha512-aX7lxZcqXldtnSr0bgT2ZPQvpTpIwBMW1GMX29o4s3q02vmq6JoTYuPW0GwYXa8BiFMRX7WbGZVDA703pNqOMQ==} + engines: {node: '>=20.19.0'} peerDependencies: - '@nestjs/common': ^10.0.0 || ^11.0.0 + '@nestjs/common': ^10.0.0 || ^11.0.0 || ^12.0.0 class-transformer: ^0.4.0 || ^0.5.0 class-validator: ^0.13.0 || ^0.14.0 || ^0.15.0 reflect-metadata: ^0.1.12 || ^0.2.0 @@ -1132,27 +1135,29 @@ packages: class-validator: optional: true - '@nestjs/platform-express@11.1.28': - resolution: {integrity: sha512-hU+9Sz4m+onHrR5AmelI59QKmY/Re546bPnygnpqqeQdHDiJpBgjWbL4t6Jr73CBpS60cpyng7WzjgphNB9iwA==} + '@nestjs/platform-express@12.0.1': + resolution: {integrity: sha512-nF8bRgROptMyixYBfiJUAsjj2o9k13zxAjUKYaqHTPk8mTSd+gk7hXz6NykXNrfHWmMEYC9RttQlMUdMHNTbuA==} peerDependencies: - '@nestjs/common': ^11.0.0 - '@nestjs/core': ^11.0.0 + '@nestjs/common': ^12.0.0 + '@nestjs/core': ^12.0.0 - '@nestjs/schematics@11.1.0': - resolution: {integrity: sha512-lVxGZ46tcdItFMoXr6vyKWlnOsm1SZm/GUqAEDvy2RL4Q4O+3bkziAhrO7Y8JLssFUUvNFEGqAizI52WAxhjDw==} + '@nestjs/schematics@12.0.0': + resolution: {integrity: sha512-ilsxam7i+SWd2psr12faLVSk86FYKnxRUGO6jAGNi1P9+O0kRaYVOScyrKeIj97W9T9UJoJF38fg+7KgjY/nOg==} + engines: {node: '>=22.12.0'} peerDependencies: prettier: ^3.0.0 - typescript: '>=4.8.2' + typescript: '>=6.0.0' peerDependenciesMeta: prettier: optional: true - '@nestjs/swagger@11.4.6': - resolution: {integrity: sha512-Le136h2WC7HGsd70+WyK1qrm+Zq7kFxBLkYC1JgAVqNRCt8kNh7bMF7Qkn65D5j2t/aks0+VbWmUVlYIwPrs3A==} + '@nestjs/swagger@12.0.1': + resolution: {integrity: sha512-fQ0WyutSSHedO9HzSkOFKFaRygYNGBno5EggcZljs+KL8tHzPPTO5oyJpaf3BoOKFGGsN/0B13EbIpH2pkPx8g==} + engines: {node: ^20.19.0 || >=22.12.0} peerDependencies: '@fastify/static': ^8.0.0 || ^9.0.0 || ^10.0.0 - '@nestjs/common': ^11.0.1 - '@nestjs/core': ^11.0.1 + '@nestjs/common': ^12.0.0 + '@nestjs/core': ^12.0.0 class-transformer: '*' class-validator: '*' reflect-metadata: ^0.1.12 || ^0.2.0 @@ -1212,70 +1217,70 @@ packages: typeorm: optional: true - '@nestjs/testing@11.1.28': - resolution: {integrity: sha512-B+VgRxeLaH7jkOMgAyUP3N3rpFlisQ7JRxixRbgHvG6a0VgKbbkNSofKExexCgKmQQak80undb3+2kE1lUBmRQ==} + '@nestjs/testing@12.0.1': + resolution: {integrity: sha512-prf3fsUaoKS6t04XcQt5aSTp68Po1GxUcbEBPv+3iqAef07hKQEO15r0jmmwA+rWZfONYT1UcV3YIylR++/tCg==} peerDependencies: - '@nestjs/common': ^11.0.0 - '@nestjs/core': ^11.0.0 - '@nestjs/microservices': ^11.0.0 - '@nestjs/platform-express': ^11.0.0 + '@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 - '@next/env@16.2.12': - resolution: {integrity: sha512-d0Z5Bc13Fa4nR8pFAKx2jay2yhJM16vlfHbTzYnUQAxlNb6B6lmn4hjt69lYNt4kRtyYP6gEM49lPRHNbIyneg==} + '@next/env@16.3.3': + resolution: {integrity: sha512-U2eYQRwXj+dsqxV79zFqExDdatnNY/ZWc2nsJU1p/OgT7fd3dXwlF6OjYaFQCfMoeTA19PWq+wVmYgimVA+V+g==} - '@next/swc-darwin-arm64@16.2.12': - resolution: {integrity: sha512-0W1R0teHWJrqKX0FH20IzzIWAOuGtBxPGuObrxy1lE8hQvCFj49KE8a3WUg0D7sq6rn6zkM4c7YGUnhudBS6oA==} + '@next/swc-darwin-arm64@16.3.3': + resolution: {integrity: sha512-8Hiv32QJPwdV6KYJ8meR9SBA061tQqnIKTJDocvOXlEQqib0xMFpzArosuffFUUc0sslbh7QQ8a3Yey1QV8EIw==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@next/swc-darwin-x64@16.2.12': - resolution: {integrity: sha512-Hy5Ls099+aFUmOLmIgPfLqNi6iCwhL3uQCssz5rWk+5Nkc6TUKCE83DY5BbNylfm3+mfwcSFnLRfrZDJhVxdtw==} + '@next/swc-darwin-x64@16.3.3': + resolution: {integrity: sha512-A1lgKgwVchRYmSe467zdwhxT9040dd8lH+o65sL5Jet8fjB4kegw/rDyPIpYVRb6jAqwXFOJpjIXJLxQKLiE3A==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@next/swc-linux-arm64-gnu@16.2.12': - resolution: {integrity: sha512-+YqU2h1cQkHsGfvjAsrSmst8UIFBibBGm5x3Xgel8NLMiDQtNOM4sM2GOEMvG5YiOBNeN/Ykk8cQC2S0Xrqljg==} + '@next/swc-linux-arm64-gnu@16.3.3': + resolution: {integrity: sha512-bf0FIssMFueU2dm7vQEWWxk0c8UjKTdW0yzuh0sQsD8pf1+KCLDdaqhYZNMYGmXwEOiHAUzgBKudovIlcvvBjg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] libc: [glibc] - '@next/swc-linux-arm64-musl@16.2.12': - resolution: {integrity: sha512-0qjhiYBaKAqF63LA1ZWAAnKTzFUguAaZiRa5etMLGGPj/B6uEVjtIZldIzFEp3wHlB0koK6aTzqPtSdplTCjoA==} + '@next/swc-linux-arm64-musl@16.3.3': + resolution: {integrity: sha512-W7viwCk9JY/cAkdz/A273rd5bb3RgT/IHwR7Upv90tunjBWNtAAhGhoecHh+teRNRSinuAFmE+l7fwZ4YKkrXg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] libc: [musl] - '@next/swc-linux-x64-gnu@16.2.12': - resolution: {integrity: sha512-7A3q26W+h7gnA15uqBToNuDqBEFZZcqh0mW2mn4AJh/G5pdg2RVE3n4slzLEliASZFG3NmsbEzng/x2Sh09mBg==} + '@next/swc-linux-x64-gnu@16.3.3': + resolution: {integrity: sha512-0W46zw1N3ODpI6n0GeivHvvob1pooozgZVqy65k0mh4/7vr+FbY9+WpHzNVXjHipJf/A3FDheBG19H1s5A25rA==} engines: {node: '>= 10'} cpu: [x64] os: [linux] libc: [glibc] - '@next/swc-linux-x64-musl@16.2.12': - resolution: {integrity: sha512-qSjL/uppm+cbh21s72Ss8gkiOhQ4dExWHNGOWy6eZV7STj5WsKehgxT61beSsOj+YYQuTplL376lOCdMQU5T8w==} + '@next/swc-linux-x64-musl@16.3.3': + resolution: {integrity: sha512-H4mBso8ZTMBPtdT0PN0pBx2ayTvQuTuvS6qT13d77yVFJXAPCxkyIhLTmdMaGTJs0krQYI/qpzdHijCeihXhbg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] libc: [musl] - '@next/swc-win32-arm64-msvc@16.2.12': - resolution: {integrity: sha512-X6hzsOUJac/e7AWSbn9gQ9nzHld1xWP5iyjHpYWvud8pufB679O1xg4JDyKr8Xd69Jvd+kM2Der6uftiZCmjYA==} + '@next/swc-win32-arm64-msvc@16.3.3': + resolution: {integrity: sha512-cTMUJpcEGmeywofCUfhR+rSsoE33+rVPnPEYNTNdLNlsOeEg/vktOsKUSTb28vUGqD2jkm4Zaskcwn7OCI6FQg==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@next/swc-win32-x64-msvc@16.2.12': - resolution: {integrity: sha512-F6fakeHuFTLOPt0bslQJdf+xtT+WIP9DVn/m4y1w1mRnVPyh3D/cNvzlRkxM444xfm+IvvYNSOrKiA2CDJ0Uxw==} + '@next/swc-win32-x64-msvc@16.3.3': + resolution: {integrity: sha512-2VR4cTBzHXaBjnGsuH6GyJjENzQOmHeAh11uY1iUhjm3j5dEUrVJuUj+VL78jaGi/Dik8xS76zEj18BsFhlVZQ==} engines: {node: '>= 10'} cpu: [x64] os: [win32] @@ -1303,6 +1308,88 @@ packages: '@paralleldrive/cuid2@2.3.1': resolution: {integrity: sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==} + '@parcel/watcher-android-arm64@2.6.0': + resolution: {integrity: sha512-trgpLSCKRC/huFjXX/Smh+0sWe4+YtKfktIToiMl59ghz7z+qkH6kMvNnUbLyRs9N11t8l4svSCs1+5B3rOAhA==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [android] + + '@parcel/watcher-darwin-arm64@2.6.0': + resolution: {integrity: sha512-Y3QV0gl7Q1zbfueunkWIERICbEojQFCgpyG7YqOGNFLsckXyI1xu9mAIUpKY9QBYzBtSkN8dBPwd3yiAO9ovMw==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [darwin] + + '@parcel/watcher-darwin-x64@2.6.0': + resolution: {integrity: sha512-Ohv6OpzhUfKYD7Beb8kDvG0jbIxORCYY1JRdZnaBtnjjkJxgD7ZVL0nw2sCYd0yTMKTvz3nnTnOF3cDifK+kvw==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [darwin] + + '@parcel/watcher-freebsd-x64@2.6.0': + resolution: {integrity: sha512-5HmXvDgs8VK+74jF9y9/2FE3/OnlcKmc56tjmSrEuZjpSZOGL+fvAu+HKJBdPs9uwoP2hE6TlSUpXZ/C5jUFmQ==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [freebsd] + + '@parcel/watcher-linux-arm-glibc@2.6.0': + resolution: {integrity: sha512-Ps/hui3A+vMbjdqlqAowK2ZL8+BO8dBjxeWXj6npTBs3jx4wWmbPpaLuqwrQrSqIVMCnpWo238bJ1U37GhQOYg==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@parcel/watcher-linux-arm-musl@2.6.0': + resolution: {integrity: sha512-9c6AUHgHoG+IY88MRIHupztQiQnrbqHYQjkM2btA+Bf/wQnQMuiD0Wfk1EVv3TlNT3x41uU71rn6E4xh/+zvkw==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + libc: [musl] + + '@parcel/watcher-linux-arm64-glibc@2.6.0': + resolution: {integrity: sha512-yHRqS2owEXe6Hic9z6Mh1ECsCd+ODVOGvZDyciqRd21+v+o+DnXMOrw50DSpIG2sb8GPEaPPmfeCAWKPJdq46g==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@parcel/watcher-linux-arm64-musl@2.6.0': + resolution: {integrity: sha512-WhB2e/V7rqdHHWZusBSPuy5Ei8S6lSz6FE5TKKQz5h3a0O+C+mhY7vxU9b/stqvMb8beLnPY82ZrFTLKs+SrKA==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@parcel/watcher-linux-x64-glibc@2.6.0': + resolution: {integrity: sha512-ulGE6x6Oz6iAwg75T8YQSoguBWasniIbX+QWpaYPcCnDOpdWX3k+4xbEYPZVLxOuoJI+svJJPD3sEj8G7lrQ3A==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@parcel/watcher-linux-x64-musl@2.6.0': + resolution: {integrity: sha512-tkBYKt7YQrjIJWYDnto2YgO8MRkjlMTSNoRHzsXinBqbLdeOM3L32wPZJvIZxqaLMfSlS/4sUjH/6STVP/XDLw==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@parcel/watcher-win32-arm64@2.6.0': + resolution: {integrity: sha512-gIZAP23jaHjGWasY/TY6yL7NHFClf0Ga7FN+iINvk+KN94rhm94lYZhFsbYFNcA04/onvGD9kKmiJLJB2HbNwQ==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [win32] + + '@parcel/watcher-win32-x64@2.6.0': + resolution: {integrity: sha512-cA+/pXV2YkfxlIcXOQ5fSWqAzzPyD78/x5qbK/I0vUkrlYHA8TIz+MXjAbGouguKVSI4bOmkTSJ1/poVSsgt+A==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [win32] + + '@parcel/watcher@2.6.0': + resolution: {integrity: sha512-7FNeNl8NCE7aINx7WXiKQrPYZWC/hvrTsmk6zmxbI7LTXE7hVek/n8AfVgpe2y82zl3w0HvCHN0bVKMBoJcC0w==} + engines: {node: '>= 10.0.0'} + '@pkgjs/parseargs@0.11.0': resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} @@ -1311,14 +1398,14 @@ packages: resolution: {integrity: sha512-SEeaJLb3qBNF/OaXnaR1NmmBbFYk1zC0ZH/52fATcRPLFg/p791YrcyFFy44Bo9sLaGuSuLp5Q6axbb/O+v/RA==} engines: {node: ^14.18.0 || >=16.0.0} - '@prisma/adapter-pg@7.9.1': - resolution: {integrity: sha512-Ho2RK1KanQxLNSC0sR5bpiiVep10sWPLXCcxK+KXfI/Q69TMRbiafSvLPv3V9snimX72rMCqGlyJ4sBO4lKTAw==} + '@prisma/adapter-pg@7.10.0': + resolution: {integrity: sha512-N7nwSor0HO1Kz6xBv0TPAjAPysKK0fac6p4fVN3ensLOuzc/83Fgmln5k92eK/cvzqdkSR/2kkAqlbcdwVrwpw==} - '@prisma/client-runtime-utils@7.9.1': - resolution: {integrity: sha512-mVIBGYdO5CFmK0HvjxrtfIyQQcPdb88pSCeVQriVQPVZyDovIWblpHfOgcS8QO187j3QF0ePArH8qPhp0AU2vg==} + '@prisma/client-runtime-utils@7.10.0': + resolution: {integrity: sha512-cnCy7lUV8/CctgKVEmqAbSLAmwqJdE/qAlqTBk/0NDk59zEb2cZ0M0M0E4vVPnqbSEYudRroQDvOWfUZH6RIfw==} - '@prisma/client@7.9.1': - resolution: {integrity: sha512-+xgrh2EhJVF79wC0yX5G4PI1Rdcm7Qn/nekNQ+t/O153wtNggruHal+fXHSa0QE+Tp/Cw5wvxeCEhZZ59xGm8Q==} + '@prisma/client@7.10.0': + resolution: {integrity: sha512-Ubw/QS9JGIBSBUsyxAUQuK/Jcu0Tsva7le7QbLd91Kix9yJvYDdj5QkwgEbbZniH80dd+sziQcALPc+HnvQC8Q==} engines: {node: ^20.19 || ^22.12 || >=24.0} peerDependencies: prisma: '*' @@ -1329,38 +1416,38 @@ packages: typescript: optional: true - '@prisma/config@7.9.1': - resolution: {integrity: sha512-4znKhxTmXmuPye9Z6pbIyYb5VZlkZ05qG1L6Dr4g+7oTwc6V50Bs9XirFBDdjWt+H/AabMn9aUnxBcvj8z05aA==} + '@prisma/config@7.10.0': + resolution: {integrity: sha512-Rcg828gIRE3HOQ3pOATFjV5d/P0U9OIobxhd/IMxlfWjA4vru0eGwb0AIwFw0rmcLMVShohZYWPixVxkBHsxUA==} '@prisma/debug@5.0.0': resolution: {integrity: sha512-3q/M/KqlQ01/HJXifU/zCNOHkoTWu24kGelMF/IBrRxm7njPqTTbwfnT1dh4JK+nuWM5/Dg1Lv00u2c0l7AHxg==} + '@prisma/debug@7.10.0': + resolution: {integrity: sha512-caygJKtltmRIgdJ3jRpkOr7yM4DW6zxo5uOmojKWFb3asnxWoRkQOwZmXBgD8FZp4htrX+nMpcWqDwzlQ1+Y4g==} + '@prisma/debug@7.2.0': resolution: {integrity: sha512-YSGTiSlBAVJPzX4ONZmMotL+ozJwQjRmZweQNIq/ER0tQJKJynNkRB3kyvt37eOfsbMCXk3gnLF6J9OJ4QWftw==} - '@prisma/debug@7.9.1': - resolution: {integrity: sha512-/cpVZ4itxtcgB8GHBvZtcmuEjq+lWsLrRJxFMbwZrT1RIdtuKmUm7PPGo/wzfbYpBrk+9WmmBE8CHJw2rybKDQ==} - '@prisma/dev@0.24.17': resolution: {integrity: sha512-UvdZzmpFwknnfreh6Jije84ekkYGPYEJhXG1tFzCsCfQyzJifrOo38eZc0qajzvaC6OLUOrN9ML5XfCnEZL9DA==} - '@prisma/driver-adapter-utils@7.9.1': - resolution: {integrity: sha512-vmHehG7nn/heW32DXXpp13DxxAxVVe6n250oEt3dOL2E/4bt3olktKZN0mzSuxMMronyMSkbeW2uCOn3F4g8RQ==} + '@prisma/driver-adapter-utils@7.10.0': + resolution: {integrity: sha512-u8zkcRLlaryO652T4qavBg0HmzNW5tSKdsCn6hc1PhWAp/J6k0vrxLuUs+b9o+HcjsK7Dfa01o4OFSn0frauJA==} - '@prisma/engines-version@7.9.0-1.e922089b7d7502aff4249d5da3420f6fa55fc6ad': - resolution: {integrity: sha512-2BsPPFksz3CQUXG6af3rVCtJKg6+JJGJTtfgu2fU8DdXhOfkBjulCq8mwybCd6ge0/jhZq2kOtLAbmUDMyI1nA==} + '@prisma/engines-version@7.10.0-4.0edf323efd1d98336f3f0a68684b56f689b900d3': + resolution: {integrity: sha512-8OJ6RuZTZ06eFUOtBwxVmv8XMmOW6HWN5F+uxUbZkGxR0Bfab1dfAdXaHPmR5mb59E+fmUo8IOzXlbLY1SClbw==} '@prisma/engines@5.0.0': resolution: {integrity: sha512-kyT/8fd0OpWmhAU5YnY7eP31brW1q1YrTGoblWrhQJDiN/1K+Z8S1kylcmtjqx5wsUGcP1HBWutayA/jtyt+sg==} - '@prisma/engines@7.9.1': - resolution: {integrity: sha512-UprXSMNXx2NF5ow4pqaQtE8OuBz6K78B0wc0tn2L28G5r933iWp1DR9Do2qWrsNvvFIP3x6mpEWnQtckMO0Uhg==} + '@prisma/engines@7.10.0': + resolution: {integrity: sha512-KNumN6NHFwybvfdYzTee9pqwx5PvknpWAaHn6L5NsbrKdl+SQrsVZs9opKs6U6SAsvB26HDt3WybRjOhgoWOYQ==} '@prisma/fetch-engine@5.0.0': resolution: {integrity: sha512-eSzHTE0KcMvM5+O1++eaMuVf4D1zwWHdqjWr6D70skCg37q7RYsuty4GFnlWBuqC4aXwVf06EvIxiJ0SQIIeRw==} - '@prisma/fetch-engine@7.9.1': - resolution: {integrity: sha512-9DwxrNTeT25Orbu9CWh0CZvVlyY1lmscpbaeLZcOnuR7zcuFrt91YSmmOfIm7zJ08YOZ6mVzURKwLoMwEBcK8w==} + '@prisma/fetch-engine@7.10.0': + resolution: {integrity: sha512-Zqyu8DY14t6W/xwmAxUYWCXtHrvQnSvT644EAZSsdM8NSmCS74vJJbBKdVsK3ucFpnUWkEpbO1a0CxJXrg130g==} '@prisma/generator-helper@5.0.0': resolution: {integrity: sha512-pufQ1mhoH6WzKNtzL79HZDoW4Ql3Lf8QEKVmBoW8e3Tdb50bxpYBYue5LBqp9vNW1xd1pgZO53cNiRfLX2d4Zg==} @@ -1368,12 +1455,12 @@ packages: '@prisma/get-platform@5.0.0': resolution: {integrity: sha512-JT/rz/jaMTggDkd9OIma50si9rPLzSFe7XSrV3mKXwtv9t+rdwx5ZhmKJd+Rz6S1vhn/291k21JLfaxOW6u8KQ==} + '@prisma/get-platform@7.10.0': + resolution: {integrity: sha512-0bra1LFYi8xNw0yqV62bHJNQk4BKleOngZiqPWIQc7a3+9q6rqsnqJ15BuepP8943PFMvtmgnP52juZsyYkA6w==} + '@prisma/get-platform@7.2.0': resolution: {integrity: sha512-k1V0l0Td1732EHpAfi2eySTezyllok9dXb6UQanajkJQzPUGi3vO2z7jdkz67SypFTdmbnyGYxvEvYZdZsMAVA==} - '@prisma/get-platform@7.9.1': - resolution: {integrity: sha512-PK8R60YZRQvYxBrGG9i7l2/rFyzy+2MuI1dKtmtrCqPH8YpiJx/MfiC7LRzX5786rZDEv7BngcjfIJW4/9ADuw==} - '@prisma/internals@5.0.0': resolution: {integrity: sha512-VGWyFk6QlSBXT8z65Alq5F3o9E8IiTtaBoa3rmKkGpZjUk85kJy3jZz4xkRv53TaeghGE5rWfwkfak26KtY5yQ==} @@ -1469,14 +1556,15 @@ packages: '@types/react': optional: true - '@rtsao/scc@1.1.0': - resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} - '@scarf/scarf@1.4.0': resolution: {integrity: sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==} - '@sinclair/typebox@0.34.49': - resolution: {integrity: sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A==} + '@sinclair/typebox@0.34.52': + resolution: {integrity: sha512-XiMQh7qqVlxZzcVD+kkGMNGMzcTrDMLWI7S4x7z1MkCkbDPrekpZXEUK0eZqZFMuHQg2a2DZOcDIh9o5v3Gonw==} + + '@sindresorhus/is@4.6.0': + resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==} + engines: {node: '>=10'} '@sinonjs/commons@3.0.1': resolution: {integrity: sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==} @@ -1484,35 +1572,35 @@ packages: '@sinonjs/fake-timers@15.4.0': resolution: {integrity: sha512-DsG+8/LscQIQg68J6Ef3dv10u6nVyetYn923s3/sus5eaGfTo1of5WMZSLf0UJc9KDuKPilPH0UDJCjvNbDNCA==} - '@smithy/core@3.31.1': - resolution: {integrity: sha512-CyogUINxvi7C7LDsh8Syo6hVJOT9ckz4rG8dRZfTJ8r91HkMY59PnNooaj7WcHyxEkxPfBAmbgztZU+xTo76lg==} + '@smithy/core@3.33.3': + resolution: {integrity: sha512-CsOeKq/9kA3y6VJHt+/+VTCtBaxJ4OTFpgrjIUhPpDIKxBci1k2bJaQASF2h/ELWrulGp+t97DZ0mevfAD8idg==} engines: {node: '>=18.0.0'} - '@smithy/credential-provider-imds@4.4.16': - resolution: {integrity: sha512-QfuLWAkLzptffFW980AFeHZFdqds2B64rpEd3uJ6lgs3xVn9QegGMUgUcj+4d7dRrAsya3r58ZKpku97WcFb4w==} + '@smithy/credential-provider-imds@4.5.2': + resolution: {integrity: sha512-A9uSdn72ozbRUSit0eib0TW7nXuNPlaeM0zcGkJ+nE6tFcSDbnmtwoxbTCFBukVQcszDAyvsd7+rTduPTXpygg==} engines: {node: '>=18.0.0'} - '@smithy/fetch-http-handler@5.6.13': - resolution: {integrity: sha512-4fW86pEUOMbrD5nkbyl/tTvPHHWJFbuB2odl6ps9lWfHoXf9HWh3Q/Smh59qH1g7+c/BSZghX6bbUk4gsiMs8A==} + '@smithy/fetch-http-handler@5.7.2': + resolution: {integrity: sha512-nZyWTmSpJEXl6VtWVMBJve/7x12DZu6sIX1z1a+ZMaHlQQRs9Zpu6NbTe/gmxYXVRpkjxyDYpZ5gx2IM6f/Wkw==} engines: {node: '>=18.0.0'} - '@smithy/node-http-handler@4.9.13': - resolution: {integrity: sha512-Nmd/Nl35zfYrd+a6OO2cDJb3GPh9bgTjIUhcM+JFfjpp8/osCgboDV5nCT1I01Pv6R13eSKDKLSoVa5ZB6Zsfw==} + '@smithy/node-http-handler@4.11.3': + resolution: {integrity: sha512-2jY1tSpERfPfWqyBV2pH+iGFaghVsIJszJNsT7hxtQYhVJpWDyc0LqOWI+nXOxOAHaEfZ4PXXtp1wW1TGpHhkA==} engines: {node: '>=18.0.0'} - '@smithy/signature-v4@5.6.12': - resolution: {integrity: sha512-I6KLtq3H0qqSuV9vLglfi8puHqzygzWHOnI4z/Rdoo+q50vvo18vBRdPAvvEtcaKROz7Zn6qnPa14kRfPH6PcQ==} + '@smithy/signature-v4@5.7.3': + resolution: {integrity: sha512-7ImGm+FkHRLcBaRttIAMZ6bzJZWb2cJGoYjq46F2UjycujWzrL9GEN9h4w7eQyXJYnltrUhxbbieBAIRrdqpow==} engines: {node: '>=18.0.0'} - '@smithy/types@4.16.1': - resolution: {integrity: sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg==} + '@smithy/types@4.17.2': + resolution: {integrity: sha512-FOKpVZob9MPTn2znRzGrnsMHv7BOsKVw3XiP/cOyYLDVZ9qKp4nifIiSCuUU/fIj5Vu0UOAxCFr+qRAtG0NUkA==} engines: {node: '>=18.0.0'} '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} - '@swc/helpers@0.5.15': - resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} + '@swc/helpers@0.5.23': + resolution: {integrity: sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==} '@tailwindcss/node@4.3.3': resolution: {integrity: sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==} @@ -1613,8 +1701,8 @@ packages: '@tokenizer/token@0.3.0': resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==} - '@tsconfig/node10@1.0.12': - resolution: {integrity: sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==} + '@tsconfig/node10@1.0.13': + resolution: {integrity: sha512-gcLdvR9HO1ZJBypsOGqaP6TFEzb6vIta0KSTLt9NAQ6pXQO3cRgSVyCN6pzYqI9DlJgY71XKO0dpDhCf08b3pg==} '@tsconfig/node12@1.0.11': resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==} @@ -1625,33 +1713,33 @@ packages: '@tsconfig/node16@1.0.4': resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} - '@turbo/darwin-64@2.10.7': - resolution: {integrity: sha512-/c9cSBRermWDv85oufLhoH6XRLOVbvzJLRd+WLyfJCP+i0HFLQj4PVNDrHcY17/ve5l8X0Oua4bJBqJUgJPnZA==} + '@turbo/darwin-64@2.10.12': + resolution: {integrity: sha512-9nKgKoF6ZOUsM+or0OtNf+TTJSfGvDNP7ZFv/ZGWVwOSCkumyctQiTeHwB4UNljHTnC41AqylgbunLDHoccNrA==} cpu: [x64] os: [darwin] - '@turbo/darwin-arm64@2.10.7': - resolution: {integrity: sha512-8lpCCGWZBl9PIF8w8f2iEWrLMbHBWIfJeV6l2UEGqysD6HRIM4ySj/8R7HGEzbECJ4r/gnJcHmxEoG8yjFe64A==} + '@turbo/darwin-arm64@2.10.12': + resolution: {integrity: sha512-H4Elb1jqTZVeIC9bbcNwjSzemZ6RegoTOVHeuV5Osirt2Z8UguTyisMEkvZjPVZgMeN9J4ERZBFad40tFnkb7w==} cpu: [arm64] os: [darwin] - '@turbo/linux-64@2.10.7': - resolution: {integrity: sha512-Midw9Ed00yw9rqkWN82fY3LmLNFQ6yiL1GXB56DJKUEjWaEd27zh7ohCxzzrjfjQVrEeVWd3UPytTAV16XDQlA==} + '@turbo/linux-64@2.10.12': + resolution: {integrity: sha512-lr7KIotukvjZwEXiFSYAeOH3BWzjFVBbSzTbv0fuGFsNukYyH0+g1hB5ecqnJkgkYU+KHEMG1edOhnjiKON1wQ==} cpu: [x64] - os: [linux] + os: [android, linux] - '@turbo/linux-arm64@2.10.7': - resolution: {integrity: sha512-UVEy+MW/xn4BcsiV3v3uv0/oObyaQgVtRT+Jj4WE53rNH05VEYEc1Z13q3zV6276wCZR4Yxc4hiTTcjw7PjSpg==} + '@turbo/linux-arm64@2.10.12': + resolution: {integrity: sha512-f0pZDTtvzB5SuNwuXBaKbZHUCMCukgc8nMlHEuvLmj91Fzec+MEbr3cAvGNor5htEDqZnO6Lxt9N/GPI/77oGA==} cpu: [arm64] - os: [linux] + os: [android, linux] - '@turbo/windows-64@2.10.7': - resolution: {integrity: sha512-l2nH9KGLV46SWjcXvyc2+xo5gdf5J0NVADknQk9OCJhzJBpiNl/byd26yIfwZBjLupdqT6UOdPhPxcpUPxRykQ==} + '@turbo/windows-64@2.10.12': + resolution: {integrity: sha512-SDOueJRjS/QcykWf2KCRtTLmIl5YMKsLbXkXQGhDwcTXvKXZiS5ih5lBl/gkwZIpYFjqA/rAlfMzlAFcVHNe0g==} cpu: [x64] os: [win32] - '@turbo/windows-arm64@2.10.7': - resolution: {integrity: sha512-qjE1apG6RThuX49vUJd5ks2dV2ndXC2qktDChQonFMvUzrMrEnZMQzO+IgxBiYq1j+NbXQcRwj84nGnk1TBw1g==} + '@turbo/windows-arm64@2.10.12': + resolution: {integrity: sha512-0i0mVUa4kKk+/B3RwEwPMf9CB+T7ul56hn5FFHNA4VUNTOoLBEd6aNf3FaKfCatDNZ6cicCEf6if9QUTVyzzcA==} cpu: [arm64] os: [win32] @@ -1741,8 +1829,8 @@ packages: '@types/estree@1.0.9': resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} - '@types/express-serve-static-core@5.1.1': - resolution: {integrity: sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==} + '@types/express-serve-static-core@5.1.3': + resolution: {integrity: sha512-dPfW8NFiOF4wOHc7+N/QSxlY9cfSsenewGbAz8C8U/MULPd/YZ27LvJUIlzaXie7e6Ove9YunJGgC9tbHD2cKw==} '@types/express@5.0.6': resolution: {integrity: sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==} @@ -1768,14 +1856,11 @@ packages: '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} - '@types/json5@0.0.29': - resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} - '@types/jsonwebtoken@9.0.10': resolution: {integrity: sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==} - '@types/lodash@4.17.24': - resolution: {integrity: sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==} + '@types/lodash@4.17.25': + resolution: {integrity: sha512-+K1NIO8I+F9/wNulfVvu23QYd0Pe9/OCqRrim4NoYIf1VoEDL90Ve4ClzpyqBLc7NpGGWRvYNCKZ1BE/Jpf8dQ==} '@types/methods@1.1.4': resolution: {integrity: sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==} @@ -1783,14 +1868,14 @@ packages: '@types/ms@2.1.0': resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} - '@types/node@26.1.2': - resolution: {integrity: sha512-Vu4a5UFA9rIIFJ7rB/Vaafh9lrCQszopTCx6KjFboXTGQbPNasehVR5TEiithSDGyd1DEiUByggTZsg8jukeIg==} + '@types/node@26.4.0': + resolution: {integrity: sha512-faiGnoIrLH/V8cibOMEAZ8pMw6oXqSukl29ra4mN8GdaB2ZewzeaLj+INpV5N+Z1eKWzY+IzaIZH2EIR6YZRNQ==} '@types/normalize-package-data@2.4.4': resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} - '@types/pg@8.20.0': - resolution: {integrity: sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow==} + '@types/pg@8.23.1': + resolution: {integrity: sha512-fKVHpikPdg4GKks3JuLEhvwSyvwzF23hnabPy6DD8ljVbC7+6J5dQzdv4arV6jqq57djnMgs1HKBxX4P8aBI3A==} '@types/qs@6.15.1': resolution: {integrity: sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==} @@ -1798,13 +1883,13 @@ packages: '@types/range-parser@1.2.7': resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} - '@types/react-dom@19.2.3': - resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} + '@types/react-dom@19.2.5': + resolution: {integrity: sha512-fMPwH9v7r/pp43yUd2/Mbiex5KouJwwR3dzHkhLREUC6764VyDsqxhAxv6OFEYR1RhjOyD1naqba8ECDBe7ZQg==} peerDependencies: '@types/react': ^19.2.0 - '@types/react@19.2.17': - resolution: {integrity: sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==} + '@types/react@19.2.18': + resolution: {integrity: sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==} '@types/readdir-glob@1.1.5': resolution: {integrity: sha512-raiuEPUYqXu+nvtY2Pe8s8FEmZ3x5yAH4VkLdihcPdalvsHltomrRC9BzuStrJ9yk06470hS0Crw0f1pXqD+Hg==} @@ -1821,8 +1906,8 @@ packages: '@types/stack-utils@2.0.3': resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} - '@types/superagent@8.1.10': - resolution: {integrity: sha512-nbt4IWXABhW0jGmmpRzCFNlbmwCTzZ2gTUsNIr+X+ItdqPms+PAJZbWsNzpS2USqXjcoNLQcO6nXo60zcPQiIg==} + '@types/superagent@8.1.11': + resolution: {integrity: sha512-KA7srSW/HENDtOw9DOqaFLgWuMqN9WgjEw62lh9dpvRaZDkhdOkazASd7X7i2eMUYLHa1U37ZttnePsH5zTDHw==} '@types/supertest@7.2.1': resolution: {integrity: sha512-4CbBvoYVLHL7+yhbYrZET0vsvuyXTC05aRe7dNQkwMzm56auceoy6Yu3K50uZmwfHna1os3CMSgM/3QVkUtPTw==} @@ -1836,67 +1921,67 @@ packages: '@types/yargs@17.0.35': resolution: {integrity: sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==} - '@typescript-eslint/eslint-plugin@8.65.0': - resolution: {integrity: sha512-IEgob78X12rHpUmtcwFsXhZdVGJtwTVP8FiCLZkR6GlYVrl2PcuB+KhCE5BlVC/eQpQnu8WXRtkHZuPar+gCRA==} + '@typescript-eslint/eslint-plugin@8.68.0': + resolution: {integrity: sha512-WASHDpCm6qO5jj9g1a+8NiW5+GCkAyLReR56/4VruYmNgfUmqpxOfZ2Yfb8xGfJPWv5Qi6LSD8sXdces3vbp/Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - '@typescript-eslint/parser': ^8.65.0 + '@typescript-eslint/parser': ^8.68.0 eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/parser@8.65.0': - resolution: {integrity: sha512-CZ4nMxWwgu1HEEFNkeaCptra9QCtkmKdgf3sWh1rl1trIhmxLilgTV4cwcbQ4wemnT4sWQN8CaKOmdYx+g2gMA==} + '@typescript-eslint/parser@8.68.0': + resolution: {integrity: sha512-fHq2VC1kpyYfvEcbiMjOpySY4WS7voEp89yAThrHRX5sm9j2lzYppCb2umFMEed4fWcyeLjHxrz0mpjNBaBxMQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/project-service@8.65.0': - resolution: {integrity: sha512-SxnPhbTsGahizDgbu7oqFH/xVtzIqMd/s+WtnSxNxJZJpLbdT5IPdzg8EZxO3+PoKahXmwJLeNQOpKJb3/bi7Q==} + '@typescript-eslint/project-service@8.68.0': + resolution: {integrity: sha512-5GQtWZCXFcFYux955pvoS02WLc49pXNlvIxocKjS0clvwo3in1RdlzVKyiqQH9vE5AKWFLTaUgeQkOrTS+0Qxw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/scope-manager@8.65.0': - resolution: {integrity: sha512-Esbl8OSYiVxBokYgWPf7VVWg/BE798wXhimnn9ML9Pt5qoDf8bfQlgjlKXR/k98+AcNzlLKYrpCcrcuZ9DZLgg==} + '@typescript-eslint/scope-manager@8.68.0': + resolution: {integrity: sha512-T5eXpcaJNg8bhjHJ8Rjp68Vq/QBteYtTKY8TZqVNPaUbuz0f6jI9t6aDkylwvalpAB9XTTFeFOjrjXAZ3YvmVA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/tsconfig-utils@8.65.0': - resolution: {integrity: sha512-j6GzGqCiRdA7Qhur2VVmKZAkBLfnHFQfx4TaJGL9RMveZqCo48jSHHO0DTgizEnGhtWnqmbtCUSrqSkdiY/0Hg==} + '@typescript-eslint/tsconfig-utils@8.68.0': + resolution: {integrity: sha512-F7zrGQfiJHojPwi8vhxZQC1tWtJzvL74cK/nqri2lk8YUXvYaYwl263xOJ69jDWPUk1hmcdoayFwk9lX09npVw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/type-utils@8.65.0': - resolution: {integrity: sha512-YjaZ7PRI5qY7ax2L3PbvX0rRyGtipAReCWs0mhhDBHjH/vl0g0BonaGXrKdKpMbIIsMIwDgbk/xzkBTyAltS5g==} + '@typescript-eslint/type-utils@8.68.0': + resolution: {integrity: sha512-X77zqoY1EjeWGs/0JNxeaMfp5C5lIz4Tw8y66F1Ne8Faq6g424sBNYM6xBAqElfGZPLpWS+CZAp0DXyKDzWiHg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/types@8.65.0': - resolution: {integrity: sha512-JSSwWNy+H0E/01jJEM+hrX6N0OFDzFzeIhHFSAS01tlVaevpG8cFyYRPhS5yjGOvBUx3sqQHVMjCL1CAZZMxBg==} + '@typescript-eslint/types@8.68.0': + resolution: {integrity: sha512-9RnpsGJjrAllCMefGVVsImJM24YurhC0Q1h4UbvivtvOqXmR/vEJge2OoE++z9m6hyg8T1Q8t5SNT6tHSbrxcg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@8.65.0': - resolution: {integrity: sha512-JboAE2swaYt4tb1fHhHTABE2K+OLy09XfcTbhnk4Pw96f9dd2e9iYsJ28gBggHlo5z5x1rkyWvcPoTuNTd4oGg==} + '@typescript-eslint/typescript-estree@8.68.0': + resolution: {integrity: sha512-OKKsD0tYmoNiU5PW2zehO1yO56jYOm1ShYlxon/Z0SJNidAkdVg86eg9ruRuoXf8xfnuWZGbwDsStkoXbZtIIA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/utils@8.65.0': - resolution: {integrity: sha512-gXiwIHsYreboxeJucHKPvgwl7dXt50mF8s1/c00cP/WoVTyWKFdtfhRWwZiXYFU5H2O8vVoSLNrexFZjYS/SGA==} + '@typescript-eslint/utils@8.68.0': + resolution: {integrity: sha512-PB5gJMMOg0Q5P1tsgWtEAqQacJXq0qEqRHDX/YJ4FaTMLfZPpHB3gjl2EJuiZyPABxmj4ZQYiY9m1bdAJ5y7tQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/visitor-keys@8.65.0': - resolution: {integrity: sha512-8C71BQkGjiMmXtop7pHVJu1l2NNShFdkCyD6a2ezzs5vU/L3LRtb69EtcteFwz0mYMPzIgOw0n6OV4VBUWZd7A==} + '@typescript-eslint/visitor-keys@8.68.0': + resolution: {integrity: sha512-YR65gGdGvTUAWLldC3xLOvOzamdGzB4A5/N8rehEaHs3Zvoe39BhgY+u0SPch1OvrVTfLcc55wsSgK2NcnTS/A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@ungap/structured-clone@1.3.2': - resolution: {integrity: sha512-5jsZFwgR5rTdKwidH9Qmat75RKwqfpKlWWB1frDkljN127mwqBu8K0PYo7/hFpF03IEJpfVPpCQDY/eDx3iHvA==} + '@ungap/structured-clone@1.4.0': + resolution: {integrity: sha512-1mEZtMKPM09vDmQt5y7YvmN2+DFTP7Tg0EWXdic8/C6VRnpb33e4ghisCIE3WZjsE2N8mf+QV1Zqh7ZFYLWInQ==} '@unrs/resolver-binding-android-arm-eabi@1.12.2': resolution: {integrity: sha512-g5T90pqg1bo/7mytQx6F4iBNC0Wsh9cu+z9veDbFjc7HjpesJFWD7QMS0NGStXM075+7dJPPVvBbpZlnrdpi/w==} @@ -2127,8 +2212,8 @@ packages: resolution: {integrity: sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==} engines: {node: '>=0.4.0'} - acorn@8.17.0: - resolution: {integrity: sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==} + acorn@8.18.0: + resolution: {integrity: sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==} engines: {node: '>=0.4.0'} hasBin: true @@ -2173,19 +2258,12 @@ packages: ajv@6.15.0: resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==} - ajv@8.18.0: - resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==} - ajv@8.20.0: resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} ansi-align@3.0.1: resolution: {integrity: sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==} - ansi-colors@4.1.3: - resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} - engines: {node: '>=6'} - ansi-escapes@4.3.2: resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} engines: {node: '>=8'} @@ -2194,8 +2272,8 @@ packages: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} - ansi-regex@6.2.2: - resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} + ansi-regex@6.3.0: + resolution: {integrity: sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ==} engines: {node: '>=12'} ansi-styles@4.3.0: @@ -2210,8 +2288,8 @@ packages: resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} engines: {node: '>=12'} - ansis@4.2.0: - resolution: {integrity: sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==} + ansis@4.3.1: + resolution: {integrity: sha512-BJ8/l4R5LRE7hW9WdSuGYrLSHi2ynxeFpDFbH0K/CgNeY/tyhk+vO6TYxXC5r5CpUhNVX310xzPsN/H9lCdfOA==} engines: {node: '>=14'} anymatch@3.1.3: @@ -2249,14 +2327,6 @@ packages: argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} - array-buffer-byte-length@1.0.2: - resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} - engines: {node: '>= 0.4'} - - array-includes@3.1.9: - resolution: {integrity: sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==} - engines: {node: '>= 0.4'} - array-timsort@1.0.3: resolution: {integrity: sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==} @@ -2264,22 +2334,6 @@ packages: resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} engines: {node: '>=8'} - array.prototype.findlastindex@1.2.6: - resolution: {integrity: sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==} - engines: {node: '>= 0.4'} - - array.prototype.flat@1.3.3: - resolution: {integrity: sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==} - engines: {node: '>= 0.4'} - - array.prototype.flatmap@1.3.3: - resolution: {integrity: sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==} - engines: {node: '>= 0.4'} - - arraybuffer.prototype.slice@1.0.4: - resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} - engines: {node: '>= 0.4'} - asap@2.0.6: resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} @@ -2287,26 +2341,18 @@ packages: resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==} engines: {node: '>=8'} - async-function@1.0.0: - resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} - engines: {node: '>= 0.4'} - async@3.2.6: resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} - available-typed-arrays@1.0.7: - resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} - engines: {node: '>= 0.4'} - aws-ssl-profiles@1.1.2: resolution: {integrity: sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==} engines: {node: '>= 6.0.0'} - axios@1.19.0: - resolution: {integrity: sha512-ht/iuYZXEjFxLH/Hkezgd7m6JKlHHXEUSneaDz8uZe1Gj5QZtCnpyDsckvAiEnT89OEbCLmnte4R4sn7P0EKFw==} + axios@1.20.0: + resolution: {integrity: sha512-r8aOh8j9cGKpgQAqpzrUHnSIc6a59Y3Xf/cv8sy1DrHCkZHzQGEuoq1tARk6qSyDdtQGSDgpb9kFlruzPvrgwg==} b4a@1.8.1: resolution: {integrity: sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==} @@ -2316,18 +2362,18 @@ packages: react-native-b4a: optional: true - babel-jest@30.4.1: - resolution: {integrity: sha512-fATAbM8piYxkiXQp3RBXmZHxZVNJZAVXXfyeyCN2Tida3+qJ8ea9UxhiJ2y4fLO90ZImKt6k9FlcH2+rLkJGhw==} + babel-jest@30.5.0: + resolution: {integrity: sha512-PrhPHlKC+MsLnuNzgIH/y1dkz1f6cSfKWaQeaG8WxLMuG44dYWQ8E9uRrsBbAGCU/3+BEFYPN4d6G3Zc5Y+waA==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} peerDependencies: '@babel/core': ^7.11.0 || ^8.0.0-0 - babel-plugin-istanbul@7.0.1: - resolution: {integrity: sha512-D8Z6Qm8jCvVXtIRkBnqNHX0zJ37rQcFJ9u8WOS6tkYOsRdHBzypCstaxWiu5ZIlqQtviRYbgnRLSoCEvjqcqbA==} - engines: {node: '>=12'} + babel-plugin-istanbul@8.0.0: + resolution: {integrity: sha512-18wCskrN3DgbuBmp1gr7LBGT8xdz5xhQQqFvFhVxbkl8VBCrMKQ2YtqBWtUal1Zrc1HTuX0011+Brjw78TCFkg==} + engines: {node: '>=18'} - babel-plugin-jest-hoist@30.4.0: - resolution: {integrity: sha512-9EdtWM/sSfXLOGLwSn+GS6pIXyBnL07/8gyJlwFXjWy4DxMOyItqyUT29d4lQiS380EZwYlX7/At4PgBS+m2aA==} + babel-plugin-jest-hoist@30.5.0: + resolution: {integrity: sha512-gtGo1B+u14jrZQv6TdSWIWkTqclboo7Qn+dFAGUOIuXLFuJKAdg3U5MIWzZnW4vfUb4dX9skmAMiby86e/SF4A==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} babel-preset-current-node-syntax@1.2.0: @@ -2335,42 +2381,38 @@ packages: peerDependencies: '@babel/core': ^7.0.0 || ^8.0.0-0 - babel-preset-jest@30.4.0: - resolution: {integrity: sha512-lBY4jxsNmCnSiu7kquw8ZC9F4+XLMOKypT3RnNHPvU2Kpd4W0xaPuLr5ZkRyOsvLYAY4yaW1ZwTW4xB7NIiZzg==} + babel-preset-jest@30.5.0: + resolution: {integrity: sha512-ZGPn5ClP4lBDpuOK8W1yQIOy359HmbnZv3suucAlIe+SEE9yDsxV4S2PjSbH2Vc97U+WmzYD7vw3kr8NsQ/i6w==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} peerDependencies: - '@babel/core': ^7.11.0 || ^8.0.0-beta.1 + '@babel/core': ^7.11.0 || ^8.0.0-beta.1 || ^8.0.0 balanced-match@4.0.4: resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} engines: {node: 18 || 20 || >=22} - bare-events@2.9.1: - resolution: {integrity: sha512-Z0oHEHAFDZkffN8Qc39zNZjQlMDkPJRyyyZieU1VH7u8c5S+qHZ2S8ixdKIAxEjfHO7FJxXmJWgteOghVanIsg==} + bare-events@2.9.2: + resolution: {integrity: sha512-AIPKioV7/Y/8KfZ3AAhjPJxLLbY49S64Ym5DakZlUg75qQiTgUq9hEJoEwa4eUezPUlXRy/i5NpsKvo9jgKmoA==} peerDependencies: bare-abort-controller: '*' peerDependenciesMeta: bare-abort-controller: optional: true - bare-fs@4.7.3: - resolution: {integrity: sha512-xRgplks8SvcKkdlv2M6Z2LZmRsmqd+x0nXXGXeMEjwdibj1HSDrlnqBRLeYdMvsgCox7Bq0e+DHwfczOfsn6IA==} - engines: {bare: '>=1.16.0'} + bare-fs@4.8.1: + resolution: {integrity: sha512-N1nnXdHZAOSstz0XiHikGS4HGMH4CnSwhqWdGQQMqqdvp4Jybm9sE3R1WVnpWVd4SFkc8ryPDBLViNLwiEqECg==} + engines: {bare: '>=1.28.0'} peerDependencies: bare-buffer: '*' peerDependenciesMeta: bare-buffer: optional: true - bare-os@3.9.3: - resolution: {integrity: sha512-fF4Q7QsyKVF5Rj0qvI8BgUNjqzC2JvQlpTaPLjVJVxYVUX5Zr9un+y3w1HmA4nNKdFmRBT8z/WmrjvXzXVerKQ==} - engines: {bare: '>=1.14.0'} - - bare-path@3.0.1: - resolution: {integrity: sha512-ghj2DSK/2e99a1anTVPCV4m4YIYtrbXhfM7V3D7XZLOTsybnYyaJloymGqssQc8l/or0UoDyRtNQkmkEF/ysgQ==} + bare-path@3.1.1: + resolution: {integrity: sha512-JprUlveX3QjApC1cTpsUOiscADftCGVWkzitbHsRqv84hzYwYHw2mbluddsq5TvI8mH/8Ov1f4BiMAdcB0oYnQ==} - bare-stream@2.13.3: - resolution: {integrity: sha512-Kc+brLqvEqGkjyfiwJmImAOqLZL7OsoLKuavx+hJjgVV3nLTOjloJyPMFxjUPerGGHrNH0fLU06jjykMLWrERQ==} + bare-stream@2.13.4: + resolution: {integrity: sha512-PcrQ8lVLbiJscNm1Kez+Yp4Gy4AHGcN1lzwjvf5NybWen7VvEgUfyfnXYJ2zNqWnzOfCb1Abq6lH8ti0syQszA==} peerDependencies: bare-abort-controller: '*' bare-buffer: '*' @@ -2383,14 +2425,14 @@ packages: bare-events: optional: true - bare-url@2.4.5: - resolution: {integrity: sha512-K+y9xF1tN+CdPu4qWwr0QiK1Al07eFPGYK5M2pDXcmHdMdgC/tT/bpmMe1hrmRHaidKLkXrC+cRNYf3XVDUhSQ==} + bare-url@2.5.2: + resolution: {integrity: sha512-L13PCJzKG8RGvx8V1/DdMi12ERhC3tprr7/8a94BxpmnRsFqxh5XZNdhtMxu5HPkRshYOOWRGY8lDP7ZhpG9Cg==} base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} - baseline-browser-mapping@2.10.42: - resolution: {integrity: sha512-c/jurFrDLyui7o1J86yLkRu4LMsTYcBohveus7/I2Hzdn9KIP2bdJPTue/lR1KH46enoPbD77GKeSYNdyPoD3Q==} + baseline-browser-mapping@2.11.20: + resolution: {integrity: sha512-H0ulySigv6icDJ1F7SjtdCD6PrhTpdYCmP0CactWy1+ekh0AFd0o1Wn5T8b+hnTmdBx19u9yhL6wvCylXMY7zw==} engines: {node: '>=6.0.0'} hasBin: true @@ -2414,16 +2456,16 @@ packages: resolution: {integrity: sha512-9gYgQKXx+1nP8mP7CzFyaUARhg7D3n1dF/FnErWmu9l6JvGpNUN278h0aSb+QjoiKSWG+iZ3uHrcqk0qrY9RQQ==} engines: {node: '>=10'} - brace-expansion@5.0.8: - resolution: {integrity: sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==} + brace-expansion@5.0.9: + resolution: {integrity: sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==} engines: {node: 20 || >=22} braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} - browserslist@4.28.4: - resolution: {integrity: sha512-MTc8i/x9jBQd1iMw2CFGS+rwMa07eYjLR0CCTLDACl9xhxy+nIs3KeML/biicXtk9JrZ6dnnTatmc7ErPXIxqw==} + browserslist@4.28.8: + resolution: {integrity: sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true @@ -2476,10 +2518,6 @@ packages: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} - call-bind@1.0.9: - resolution: {integrity: sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==} - engines: {node: '>= 0.4'} - call-bound@1.0.4: resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} engines: {node: '>= 0.4'} @@ -2496,13 +2534,17 @@ packages: resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} engines: {node: '>=10'} - caniuse-lite@1.0.30001803: - resolution: {integrity: sha512-g/uHREV2ZpK9qMalCsWaxmA6ol+DX8GYhuf3T40RKoP+oL7vhRJh8LNt73PCjpnR6l14FzfPrB5Yux4PKm2meg==} + caniuse-lite@1.0.30001810: + resolution: {integrity: sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==} chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} + chalk@5.6.2: + resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + char-regex@1.0.2: resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} engines: {node: '>=10'} @@ -2537,8 +2579,8 @@ packages: resolution: {integrity: sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==} engines: {node: '>=8'} - cjs-module-lexer@2.2.0: - resolution: {integrity: sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==} + cjs-module-lexer@2.2.1: + resolution: {integrity: sha512-Ca8swihM+/4yKecYHY52kgJd300hi2lADU/a1RxNTRe+RJ9jvqQlESpbz9DnG9mowez8qwXHB8qYdIUw9e+F5Q==} class-transformer@0.5.1: resolution: {integrity: sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==} @@ -2557,13 +2599,13 @@ packages: resolution: {integrity: sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==} engines: {node: '>=6'} - cli-cursor@3.1.0: - resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} - engines: {node: '>=8'} + cli-cursor@5.0.0: + resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} + engines: {node: '>=18'} - cli-spinners@2.9.2: - resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} - engines: {node: '>=6'} + cli-spinners@3.4.0: + resolution: {integrity: sha512-bXfOC4QcT1tKXGorxL3wbJm6XJPDqEnij2gQ2m7ESQuE+/z9YFIWnl/5RpTiKWbMq3EVKR4fRLJGn6DVfu0mpw==} + engines: {node: '>=18.20'} cli-table3@0.6.5: resolution: {integrity: sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==} @@ -2584,10 +2626,6 @@ packages: resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} engines: {node: '>=12'} - clone@1.0.4: - resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} - engines: {node: '>=0.8'} - cluster-key-slot@1.1.1: resolution: {integrity: sha512-rwHwUfXL40Chm1r08yrhU3qpUvdVlgkKNeyeGPOxnW8/SyVDvgRaed/Uz54AqWNaTCAThlj6QAs3TZcKI0xDEw==} engines: {node: '>=0.10.0'} @@ -2610,13 +2648,13 @@ packages: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} engines: {node: '>= 0.8'} + commander@15.0.0: + resolution: {integrity: sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg==} + engines: {node: '>=22.12.0'} + commander@2.20.3: resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} - commander@4.1.1: - resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} - engines: {node: '>= 6'} - comment-json@5.0.0: resolution: {integrity: sha512-uiqLcOiVDJtBP8WGkZHEP+FZIhTzP1dxvn59EfoYUi9gqupjrBWVQkO2atDrbnKPwLeotFYDsuNb26uBMqB+hw==} engines: {node: '>= 6'} @@ -2650,8 +2688,8 @@ packages: resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} engines: {node: '>= 0.6'} - content-type@2.0.0: - resolution: {integrity: sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==} + content-type@2.1.0: + resolution: {integrity: sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==} engines: {node: '>=18'} convert-source-map@2.0.0: @@ -2711,10 +2749,6 @@ packages: create-require@1.1.1: resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} - cross-spawn@7.0.3: - resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} - engines: {node: '>= 8'} - cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -2778,26 +2812,6 @@ packages: resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} engines: {node: '>= 12'} - data-view-buffer@1.0.2: - resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==} - engines: {node: '>= 0.4'} - - data-view-byte-length@1.0.2: - resolution: {integrity: sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==} - engines: {node: '>= 0.4'} - - data-view-byte-offset@1.0.1: - resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==} - engines: {node: '>= 0.4'} - - debug@3.2.7: - resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - debug@4.3.4: resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} engines: {node: '>=6.0'} @@ -2835,17 +2849,6 @@ packages: resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} engines: {node: '>=0.10.0'} - defaults@1.0.4: - resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} - - define-data-property@1.1.4: - resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} - engines: {node: '>= 0.4'} - - define-properties@1.2.1: - resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} - engines: {node: '>= 0.4'} - defu@6.1.7: resolution: {integrity: sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==} @@ -2890,26 +2893,14 @@ packages: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} - doctrine@2.1.0: - resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} - engines: {node: '>=0.10.0'} - - dotenv-expand@12.0.3: - resolution: {integrity: sha512-uc47g4b+4k/M/SeaW1y4OApx+mtLWl92l5LMPP0GNXctZqELk+YGgOPIIC5elYmUH4OuoK3JLhuRUYegeySiFA==} + dotenv-expand@13.0.0: + resolution: {integrity: sha512-aBfBS8eYIeXmpHI9ThIlA7/WLq+SLt18iXUZhb52rW89QLKQFoIpPG1bPeewoPZsTyjSSO3T7234FBVUM1V2rA==} engines: {node: '>=12'} dotenv@16.0.3: resolution: {integrity: sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==} engines: {node: '>=12'} - dotenv@16.6.1: - resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} - engines: {node: '>=12'} - - dotenv@17.4.1: - resolution: {integrity: sha512-k8DaKGP6r1G30Lx8V4+pCsLzKr8vLmV2paqEj1Y55GdAgJuIqpRp5FfajGF8KtwMxCz9qJc6wUIJnm053d/WCw==} - engines: {node: '>=12'} - dotenv@17.4.2: resolution: {integrity: sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==} engines: {node: '>=12'} @@ -2930,8 +2921,8 @@ packages: effect@3.20.0: resolution: {integrity: sha512-qMLfDJscrNG8p/aw+IkT9W7fgj50Z4wG5bLBy0Txsxz8iUHjDIkOgO3SV0WZfnQbNG2VJYb0b+rDLMrhM4+Krw==} - electron-to-chromium@1.5.387: - resolution: {integrity: sha512-TaxwufTFDufvPEoXdhwVrA3UdFWBeWGkYoJ1K8ldF1xe6gKfth6iRNS5lTQ5JPNOHdGQm8PT1QYKUqFLCiUefQ==} + electron-to-chromium@1.5.416: + resolution: {integrity: sha512-K6bvB2BjnNrugtIih6ewlbBI9DXa976jIdiIlRLHhBoEI9a4JaQjjHyF+A1IQI543aQYR4LnmOrT/K5fZj0aPA==} elkjs@0.11.1: resolution: {integrity: sha512-zxxR9k+rx5ktMwT/FwyLdPCrq7xN6e4VGGHH8hA01vVYKjTFik7nHOxBnAYtrgYUB1RpAiLvA1/U2YraWxyKKg==} @@ -2946,6 +2937,9 @@ packages: emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + emojilib@2.4.0: + resolution: {integrity: sha512-5U0rVMU5Y2n2+ykNLQqMoqklN9ICBT/KsvC1Gz6vqHbz2AXXGkG+Pm5rMWk/8Vjrr/mY9985Hi8DYzn1F09Nyw==} + empathic@2.0.0: resolution: {integrity: sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==} engines: {node: '>=14'} @@ -2957,12 +2951,8 @@ packages: end-of-stream@1.4.5: resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} - enhanced-resolve@5.24.1: - resolution: {integrity: sha512-7DdUaTjmNwMcH2gLr1qycesKII3BK4RLy/mdAb7x10Lq7bR4aNKHt1BR1ZALSv0rPM/hF5wYF0PhGop/rJm8vw==} - engines: {node: '>=10.13.0'} - - enhanced-resolve@5.24.3: - resolution: {integrity: sha512-PwKooW9JUzh5chmYfHM3IQl5OkK2u2Nm011MgeZrss3JmFraUx/fqrf78kk8GUMYoibx/14MdwTl/1WKkG7TpQ==} + enhanced-resolve@5.24.5: + resolution: {integrity: sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A==} engines: {node: '>=10.13.0'} env-paths@2.2.1: @@ -2976,14 +2966,6 @@ packages: error-ex@1.3.4: resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} - es-abstract-get@1.0.0: - resolution: {integrity: sha512-6PMWXpdhshVvFp+FoWYs1EvG1Nj0tvk0dZM+XcK0xMEM1czRVcP6ohqPWHy6qPagSpC8j4+p89WXlT+xXJs/fg==} - engines: {node: '>= 0.4'} - - es-abstract@1.24.2: - resolution: {integrity: sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==} - engines: {node: '>= 0.4'} - es-define-property@1.0.1: resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} engines: {node: '>= 0.4'} @@ -2992,8 +2974,8 @@ packages: resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} engines: {node: '>= 0.4'} - es-module-lexer@2.3.0: - resolution: {integrity: sha512-KLdwQm2NvGLDkQDCGvmiQrhkd0JbMzXthwQAUgWjQuQdBLFa3eiBP5arXZyA+f8x+x7OXgud6bq2rxjGtHV2tw==} + es-module-lexer@2.3.2: + resolution: {integrity: sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==} es-object-atoms@1.1.2: resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==} @@ -3003,13 +2985,8 @@ packages: resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} engines: {node: '>= 0.4'} - es-shim-unscopables@1.1.0: - resolution: {integrity: sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==} - engines: {node: '>= 0.4'} - - es-to-primitive@1.3.4: - resolution: {integrity: sha512-yPDz7wqpg1/mmHLmS3tcfTfbw5f1eryXvyghYBffGdERwe+mV7ZcWzTR8LR17Kvqt3qfPurjlonmnq3MKXIOXw==} - engines: {node: '>= 0.4'} + es-toolkit@1.51.0: + resolution: {integrity: sha512-zC2lQGkM7QX+Gm6iM3+WIdZJzthsEd14LvRNJneSO2hzyz/zNBENR8+YXWo1cKxgPBtV6ksPYHELbcwBRzmdCw==} escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} @@ -3026,40 +3003,6 @@ packages: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} - eslint-import-resolver-node@0.3.10: - resolution: {integrity: sha512-tRrKqFyCaKict5hOd244sL6EQFNycnMQnBe+j8uqGNXYzsImGbGUU4ibtoaBmv5FLwJwcFJNeg1GeVjQfbMrDQ==} - - eslint-module-utils@2.14.0: - resolution: {integrity: sha512-W2WCRZ9Dqntd+2u8jJcVMV2PKulc6RdLgUUoh/yQr3uB6lo/ZOeGx11sv60/8S4QFFKNslAlWhr9u0Ef7ZW6Ig==} - engines: {node: '>=4'} - peerDependencies: - '@typescript-eslint/parser': '*' - eslint: '*' - eslint-import-resolver-node: '*' - eslint-import-resolver-typescript: '*' - eslint-import-resolver-webpack: '*' - peerDependenciesMeta: - '@typescript-eslint/parser': - optional: true - eslint: - optional: true - eslint-import-resolver-node: - optional: true - eslint-import-resolver-typescript: - optional: true - eslint-import-resolver-webpack: - optional: true - - eslint-plugin-import@2.32.0: - resolution: {integrity: sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==} - engines: {node: '>=4'} - peerDependencies: - '@typescript-eslint/parser': '*' - eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9 - peerDependenciesMeta: - '@typescript-eslint/parser': - optional: true - eslint-plugin-simple-import-sort@14.0.0: resolution: {integrity: sha512-NUJO0+XFCkk+o5EsAJruTgnfMEpeWrPWeJS15UVF60GgXmqz1BJ9/3hzlvG7lkL8Bubzos5cCLptThbFfPnSMQ==} peerDependencies: @@ -3090,8 +3033,8 @@ packages: resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} - eslint@10.8.0: - resolution: {integrity: sha512-nuKKvN+oIBO0koN7Tm7dlkmnkc21mtt0QJLwAKzjLq14y6lRTdVG36MZHJ8eQHwdJMwZbQNMlPOYedMq/oVJvQ==} + eslint@10.9.1: + resolution: {integrity: sha512-9VaAkDURekixUQJy0oJYl2DcN6oKMfxay7XzaGYAWQwsb6qfKf+x76R2k1L8kb1boc+FyCAaTA9GmiKaaiaF+A==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} hasBin: true peerDependencies: @@ -3152,16 +3095,16 @@ packages: resolution: {integrity: sha512-+I6B/IkJc1o/2tiURyz/ivu/O0nKNEArIUB5O7zBrlDVJr22SCLH3xTeEry428LvFhRzIA1g8izguxJ/gbNcVQ==} engines: {node: '>= 0.8.0'} - expect@30.4.1: - resolution: {integrity: sha512-PMARsyh/JtqC20HoGqlFcIlQAyqUtW4PlI1rup1uhYJtKuwAjbvWi3GQMAn+STdHum/dk8xrKfUM1+5SAwpolA==} + expect@30.5.0: + resolution: {integrity: sha512-8fiMWcEjPU7B9nErC4FtFcCzf2tC6I75Qf7m8wzBAWC2taZmcno3yAFEjIQL34SwoGZNgPf63UDiJLyh4SMPaw==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} express@5.2.1: resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} engines: {node: '>= 18'} - exsolve@1.1.0: - resolution: {integrity: sha512-D+42+T12DdIlJM3uepa55qGiL3sYdLBOxIl2ifQCzCHz4c7eiolaHsi3BIqEr7JxBzxv2pYZQX9kw16ziMcEmw==} + exsolve@1.1.1: + resolution: {integrity: sha512-9U/jZUgjnSGyntRr6y5Muu1MJcwFl6kPu7k8qLF0IMNfLqvw0NZ4nnVDq0RVoZ0RvCyumib4Ez3KYrVfilrw+g==} extend@3.0.2: resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} @@ -3195,14 +3138,23 @@ packages: fast-safe-stringify@2.1.1: resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} - fast-uri@3.1.4: - resolution: {integrity: sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw==} + fast-string-truncated-width@3.0.3: + resolution: {integrity: sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==} + + fast-string-width@3.0.2: + resolution: {integrity: sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==} + + fast-uri@3.1.5: + resolution: {integrity: sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==} + + fast-wrap-ansi@0.2.2: + resolution: {integrity: sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==} fast-write-atomic@0.2.1: resolution: {integrity: sha512-WvJe06IfNYlr+6cO3uQkdKdy3Cb1LlCJSF8zRs2eT8yuhdbSlR9nIt+TgQ92RUxiRrQm+/S7RARnMfCs5iuAjw==} - fastq@1.20.1: - resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} + fastq@1.20.3: + resolution: {integrity: sha512-XKv5nnLs6nLF71NgiKJLIZFLkPyIEuOselLG7ujZnGrRfQK8HpvY+WqKhAJUAdLomwVHErVS4LfxFlPq0/FTAw==} fb-watchman@2.0.2: resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==} @@ -3224,9 +3176,9 @@ packages: resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} engines: {node: '>=16.0.0'} - file-type@21.3.4: - resolution: {integrity: sha512-Ievi/yy8DS3ygGvT47PjSfdFoX+2isQueoYP1cntFW1JLYAuS4GD7NUPGg4zv2iZfV52uDyk5w5Z0TdpRS6Q1g==} - engines: {node: '>=20'} + file-type@22.0.2: + resolution: {integrity: sha512-0H8TsCUGBLx+V5adH3EY52hTAcyLKbV1D4gq5cIOJ6DnQAHeV9Z2Hhuc5CoBX4YmvB2oL+JIC84z0qO7JsCoNw==} + engines: {node: '>=22'} fill-range@7.1.1: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} @@ -3240,8 +3192,8 @@ packages: resolution: {integrity: sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==} engines: {node: '>=8'} - find-my-way@9.7.0: - resolution: {integrity: sha512-f2JHn75x2JlwUwLenZypgczR7YWMb/uO9BvUXtus+JMgkbIkLADd38cI4EiV+OQqrGo1Zlq6V8wnqMJ8e62wUQ==} + find-my-way@9.9.0: + resolution: {integrity: sha512-sJsgZ1sQH2UDuowPuMKg8az7Qc8F0jnj+SKkFWU/+T0xcFlgV5skgXOGUqmQzOdmW6ALA7AhJINWx3qFBkbLHA==} engines: {node: '>=20'} find-up@4.1.0: @@ -3256,8 +3208,8 @@ packages: resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} engines: {node: '>=16'} - flatted@3.4.2: - resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} + flatted@3.4.4: + resolution: {integrity: sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==} follow-redirects@1.16.0: resolution: {integrity: sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==} @@ -3268,10 +3220,6 @@ packages: debug: optional: true - for-each@0.3.5: - resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} - engines: {node: '>= 0.4'} - foreground-child@3.3.1: resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} engines: {node: '>=14'} @@ -3302,15 +3250,12 @@ packages: fp-ts@2.16.0: resolution: {integrity: sha512-bLq+KgbiXdTEoT1zcARrWEpa5z6A/8b7PcDW7Gef3NSisQ+VS7ll2Xbf1E+xsgik0rWub/8u0qP/iTTjj+PhxQ==} - framer-motion@12.43.0: - resolution: {integrity: sha512-1eaL3RvR/kAlbG7UYcpMptEyzPoENO0c6w7ZnB3/hh2vSAz/6uGAFn6fdoqTBguNstf3MsFhJHsD/0DHiclG+g==} + framer-motion@13.1.1: + resolution: {integrity: sha512-B/xn2TPS4f61cEBLFjiYlQFnBZUW1YVj/LM+C+N4OP8Rs95VLEI2ot/RlfBg111la/EiyECFaJJi/A3FWA8MUA==} peerDependencies: - '@emotion/is-prop-valid': '*' react: ^18.0.0 || ^19.0.0 react-dom: ^18.0.0 || ^19.0.0 peerDependenciesMeta: - '@emotion/is-prop-valid': - optional: true react: optional: true react-dom: @@ -3340,27 +3285,15 @@ packages: fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} - fsevents@2.3.3: - resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} - engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} - os: [darwin] - function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} - function.prototype.name@1.2.0: - resolution: {integrity: sha512-jObKIik1P2QjPHP5nz5BaOtUlfgS0fWo8IUByNXkM+o+02sJOi94em77GwJKQSJ3gfPHdgzLNrHc1uokV4P/ew==} - engines: {node: '>= 0.4'} - - functions-have-names@1.2.3: - resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} - gaxios@7.1.3: resolution: {integrity: sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ==} engines: {node: '>=18'} - gaxios@7.2.0: - resolution: {integrity: sha512-CUVb4wcYe+771XevyH6HtGmXFAGGKkIC3kswAP8Z1JCe0j80JMaTPZH930DWFrvo0atjh18Arc0pEyUCWa5bfg==} + gaxios@7.3.1: + resolution: {integrity: sha512-kB3rzJV7d9juLZh8/56QTXCwQfxyhdOMdyYk1HdQKFtF8TJTDTZQJtixWIwXdE9Jji91mC41DUNpjleo4L4eAQ==} engines: {node: '>=18'} gcp-metadata@8.1.2: @@ -3370,10 +3303,6 @@ packages: generate-function@2.3.1: resolution: {integrity: sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==} - generator-function@2.0.1: - resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==} - engines: {node: '>= 0.4'} - gensync@1.0.0-beta.2: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} @@ -3382,6 +3311,10 @@ packages: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} + get-east-asian-width@1.6.0: + resolution: {integrity: sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==} + engines: {node: '>=18'} + get-intrinsic@1.3.0: resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} engines: {node: '>= 0.4'} @@ -3401,10 +3334,6 @@ packages: resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} engines: {node: '>=10'} - get-symbol-description@1.1.0: - resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} - engines: {node: '>= 0.4'} - giget@3.3.1: resolution: {integrity: sha512-r+mvuDjrjMpsdw46Kmeydb8bdHm7wOKw8wNBtTndkjbPjgAp5oUJUxRE76wZFknxIPokfWvep2qSXK37aXE6zg==} hasBin: true @@ -3442,10 +3371,6 @@ packages: resolution: {integrity: sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==} engines: {node: '>=10'} - globalthis@1.0.4: - resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} - engines: {node: '>= 0.4'} - globby@11.1.0: resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} engines: {node: '>=10'} @@ -3458,12 +3383,12 @@ packages: resolution: {integrity: sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==} engines: {node: '>=14'} - googleapis-common@8.0.2: - resolution: {integrity: sha512-5MXeQzIZaqCH7B+HJWqhQm946VARpZep6acbWSr/fcgF2cQANq7allgX+i/G0EqF0WyUxB277gtWMzRYHMl9tg==} - engines: {node: '>=18.0.0'} + googleapis-common@8.0.3: + resolution: {integrity: sha512-7g1yzQKx0mmNTjiK0H9dJ8eqKqDBveES9vLHeg5neb3BMQy/d1oQefIMhIpOVT8a+f+LOcixMEdRbFIW/cQUJw==} + engines: {node: '>=18'} - googleapis@173.0.0: - resolution: {integrity: sha512-xEJJYLZ4qeenVyfzispNfRjCe9bsv7CzBv5zYFLvScOze9snJ8S9W6hjQ729CWPQt5mvn/JrcRaCHzQiukt0ng==} + googleapis@176.0.0: + resolution: {integrity: sha512-v24+WfeIw3R9iF8LT7eixlawezy5SGGNB/WjFV8qeiBS9G6KCsqaFngsnTXvBrVH4womehN2Co8UYJxZ0KpbvQ==} engines: {node: '>=18'} gopd@1.2.0: @@ -3484,21 +3409,10 @@ packages: engines: {node: '>=0.4.7'} hasBin: true - has-bigints@1.1.0: - resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} - engines: {node: '>= 0.4'} - has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} - has-property-descriptors@1.0.2: - resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} - - has-proto@1.2.0: - resolution: {integrity: sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==} - engines: {node: '>= 0.4'} - has-symbols@1.1.0: resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} engines: {node: '>= 0.4'} @@ -3569,8 +3483,8 @@ packages: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} - ignore@7.0.5: - resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + ignore@7.0.7: + resolution: {integrity: sha512-dML0wP6oak21rsNYCJpJB6O1BJIEwNpGrTw0URPfAk4hm0e3pRfCtzkfB6olBcXcVlU2rouCyz7lCyRB0OMVCA==} engines: {node: '>= 4'} import-fresh@3.3.1: @@ -3601,74 +3515,34 @@ packages: resolution: {integrity: sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==} engines: {node: '>=10'} - internal-slot@1.1.0: - resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} - engines: {node: '>= 0.4'} - internmap@2.0.3: resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} engines: {node: '>=12'} - ioredis@5.11.1: - resolution: {integrity: sha512-ehuGcf94bQXhfagULNXrJdfnWO38v070jxSx/qE87Kjzmu2fU7ro5EFAb+OPituLqgfyuQaym5DlrNydW2sJ9A==} - engines: {node: '>=12.22.0'} + ioredis@6.0.0: + resolution: {integrity: sha512-f+Dtubxfpf6KYFq7WVXJoOLn0bk4TJrMrN9SzeE+jrWrCWj7XX3fA6vkryafhADX+GMymRxgDJDOI33COkJc0w==} + engines: {node: '>=20.0.0'} ipaddr.js@1.9.1: resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} engines: {node: '>= 0.10'} - is-array-buffer@3.0.5: - resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} - engines: {node: '>= 0.4'} - is-arrayish@0.2.1: resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} - is-async-function@2.1.1: - resolution: {integrity: sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==} - engines: {node: '>= 0.4'} - - is-bigint@1.1.0: - resolution: {integrity: sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==} - engines: {node: '>= 0.4'} - - is-boolean-object@1.2.2: - resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==} - engines: {node: '>= 0.4'} - - is-callable@1.2.7: - resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} - engines: {node: '>= 0.4'} - is-core-module@2.16.2: resolution: {integrity: sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==} engines: {node: '>= 0.4'} - is-data-view@1.0.2: - resolution: {integrity: sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==} - engines: {node: '>= 0.4'} - - is-date-object@1.1.0: - resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==} - engines: {node: '>= 0.4'} - is-docker@2.2.1: resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} engines: {node: '>=8'} hasBin: true - is-document.all@1.0.0: - resolution: {integrity: sha512-+XSoyS05OdBbhFuELhgTCpFNHkpBOJqtsZfUFFpe5QTw+9Sjbh8zitxhQkYAo6wV7e1Vb8cAPvpCk9jGam/82g==} - engines: {node: '>= 0.4'} - is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} - is-finalizationregistry@1.1.1: - resolution: {integrity: sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==} - engines: {node: '>= 0.4'} - is-fullwidth-code-point@3.0.0: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} engines: {node: '>=8'} @@ -3677,29 +3551,13 @@ packages: resolution: {integrity: sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==} engines: {node: '>=6'} - is-generator-function@1.1.2: - resolution: {integrity: sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==} - engines: {node: '>= 0.4'} - is-glob@4.0.3: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} - is-interactive@1.0.0: - resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==} - engines: {node: '>=8'} - - is-map@2.0.3: - resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} - engines: {node: '>= 0.4'} - - is-negative-zero@2.0.3: - resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==} - engines: {node: '>= 0.4'} - - is-number-object@1.1.1: - resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==} - engines: {node: '>= 0.4'} + is-interactive@2.0.0: + resolution: {integrity: sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==} + engines: {node: '>=12'} is-number@7.0.0: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} @@ -3719,18 +3577,6 @@ packages: is-property@1.0.2: resolution: {integrity: sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==} - is-regex@1.2.1: - resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} - engines: {node: '>= 0.4'} - - is-set@2.0.3: - resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==} - engines: {node: '>= 0.4'} - - is-shared-array-buffer@1.0.4: - resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==} - engines: {node: '>= 0.4'} - is-stream@2.0.1: resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} engines: {node: '>=8'} @@ -3739,33 +3585,9 @@ packages: resolution: {integrity: sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==} engines: {node: '>=18'} - is-string@1.1.1: - resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==} - engines: {node: '>= 0.4'} - - is-symbol@1.1.1: - resolution: {integrity: sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==} - engines: {node: '>= 0.4'} - - is-typed-array@1.1.15: - resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==} - engines: {node: '>= 0.4'} - - is-unicode-supported@0.1.0: - resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} - engines: {node: '>=10'} - - is-weakmap@2.0.2: - resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} - engines: {node: '>= 0.4'} - - is-weakref@1.1.1: - resolution: {integrity: sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==} - engines: {node: '>= 0.4'} - - is-weakset@2.0.4: - resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==} - engines: {node: '>= 0.4'} + is-unicode-supported@2.1.0: + resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==} + engines: {node: '>=18'} is-windows@1.0.2: resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==} @@ -3778,9 +3600,6 @@ packages: isarray@1.0.0: resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} - isarray@2.0.5: - resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} - isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} @@ -3811,16 +3630,16 @@ packages: jackspeak@3.4.3: resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} - jest-changed-files@30.4.1: - resolution: {integrity: sha512-IuctmYrxi21iOSOaIXpJWalHyPAsVv0GeBHKDn8C1CA4W5htHn7INL+wdnL4Bo0+olEndvAFkmb++tIQJG+vvg==} + jest-changed-files@30.5.0: + resolution: {integrity: sha512-dq1x8JiEnHkJDxxOrF6UJDivBRAQMwAa5tzr+VX3um0SfyMFseUPQUbe51wLwggfoa5h/EmOtSpdJxxkzSlNRQ==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - jest-circus@30.4.2: - resolution: {integrity: sha512-rvHH7VlY6LgbJXJTQ87GW62g1FntOtbhh0zT+v04kC+pgL6aBKyYINXxWukCpj3dcIBMw5/XUbtDS9dU9JTXeQ==} + jest-circus@30.5.0: + resolution: {integrity: sha512-T3v7uM4wwCu+RQicjsAWCgoL3CiyuX3THSKwv2uMb9N2bMUgb+HfwDWEUma85vOGbvQNQ/YfoCXF1OCZot0ZEw==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - jest-cli@30.4.2: - resolution: {integrity: sha512-jfA2ocvVHMXS2QijrJ0d31ektP+d/W0T5RpcTX2Pq+3sVqHlsXVCM2+FmwpL+bdY8OfHpIg9xMxLF17Zg0U49Q==} + jest-cli@30.5.0: + resolution: {integrity: sha512-QHZMiy32x2K+NzJ1AuuoCAVc1Y5co0VXib3R7kD9MWcWFflzsD1eJN0wR+mkNlM+ts7C+Bjz0oOoFE5IiHylCg==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} hasBin: true peerDependencies: @@ -3829,8 +3648,8 @@ packages: node-notifier: optional: true - jest-config@30.4.2: - resolution: {integrity: sha512-rNHAShJQqQwFNoL0hbf3BphSBOWnpOUAKvidLS/AjNVLPfoj5mSf4jQMfW3cYOs6hXeZC7nF7mDHaBnbxELOzg==} + jest-config@30.5.0: + resolution: {integrity: sha512-gYQl2FqYgiVpyuB7DutBIbJRWaq5VcHdzOXJJ51HNa8J5JrsZehIlzNFW0/9s5uvvcbzM5/LTUizYSbsFErv+w==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} peerDependencies: '@types/node': '*' @@ -3844,97 +3663,88 @@ packages: ts-node: optional: true - jest-diff@30.4.1: - resolution: {integrity: sha512-CRpFK0RtLriVDGcPPAnR6HMVI8bSR2jnUIgralhauzYQZIb4RH9AtEInTuQr65LmmGggGcRT6HIASxwqsVsmlA==} + jest-diff@30.5.0: + resolution: {integrity: sha512-QjCfDMwdPFvLxTQmS4/Dswx3PUCiqmSXVLGljMC3SU7YG1qHVoR6b86IH/O2G9k9OMyKXz2vS2Q60VnAozNDwA==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - jest-docblock@30.4.0: - resolution: {integrity: sha512-ZPMabUZCx5MpbZ2eBYSvZ0J8fvo3dR9oM+eeUpb3aKNQFuS2tu3Duw1TNlMoP8k3WQgKGJuhcMFvwcVuq6T7oA==} + jest-docblock@30.5.0: + resolution: {integrity: sha512-NwDqcxtoZi33RhuW+zJS/RVA3rmheQ8BnwpYZuc/Eruaz6seQb7+aoCeDZu/3X7W2XmD8DbSo9Pn72DbKsBFYw==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - jest-each@30.4.1: - resolution: {integrity: sha512-/8MJbH6fuj48TstjrMf+u/pd06Qezz5xOXvZA6442heNOWr8bdeoGZX2d9fCn028CoMgYmroH9//zky5GfyYmA==} + jest-each@30.5.0: + resolution: {integrity: sha512-NiMFNhRygJEFqYNt8pnkxppUF6CR486GEpt9rSU6lPBf7KeccaOL0zbjxG8fgJgD//6e7zYdssnlt6j+1kI31A==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - jest-environment-node@30.4.1: - resolution: {integrity: sha512-4FZYVOk85hz2AyT6BbarKy9u37g6DbrDyCdFhsnDdXqyrueYQvB+0zO4f/kqLCRD0BsPRXPMNJeQwihKZV8naw==} + jest-environment-node@30.5.0: + resolution: {integrity: sha512-bTc79ywKLz0ogbT3JIYuEhgWV4Ffd/cJe06co4v8CyRtlmju8x5gokMlGFR8ARWhmk5SnbDRxmf50XWnlZVhDg==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - jest-haste-map@30.4.1: - resolution: {integrity: sha512-rFrcONd8jeFsyw+Z9CrScJgglRf2+NFmNam8dKu7n+SoHqNYT47mn0DdEcVUZJpvh7Iz6/si7f7yUH7GJHVgnw==} + jest-haste-map@30.5.0: + resolution: {integrity: sha512-0FStogBslBVOEqTOJr4oXMtFitmrWp9WscG6Gbns88i0YAuMXijCT2G5VMfg/HCR4QAnL+OF2C2ednag+HlDuA==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - jest-leak-detector@30.4.1: - resolution: {integrity: sha512-IpmyiioeHxiWDhesHnUFmOxcTzwCwKpgACgWajtAP+nYQXiY7DakTxB6Bx9JFiRMljr0AX1PvnQdaU1KFoz6NQ==} + jest-leak-detector@30.5.0: + resolution: {integrity: sha512-Mq11ceAkNR250Iv45RoOwuG9fb4kYbJ02qoyL7A0nCI8FV5+aG/THmcEUx5uR2dNSa4KOtz+xBKrJ8cZPhPpuQ==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - jest-matcher-utils@30.4.1: - resolution: {integrity: sha512-zvYfX5CaeEkFrrLS9suWe9rvJrm9J1Iv3ua8kIBv9GEPzcnsfBf0bob37la7s67fs0nlBC3EuvkOLnXQKxtx4A==} + jest-matcher-utils@30.5.0: + resolution: {integrity: sha512-EfaYMC9f9ds7fahB/LYFTgd1Z2RS9Vpm2e46gazij0onkpoQG7Daq+MLm8/gQVqWwRVjL/RNDggbFx9MsrJEmQ==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - jest-message-util@30.4.1: - resolution: {integrity: sha512-kwCKIvq0MCW1HzLoGola9Te6JUdzgV0loyKJ3Qghrkz9i5/RRIHsL95BMQc2HBBhlBKC4j22K9p11TGHH8RBpQ==} + jest-message-util@30.5.0: + resolution: {integrity: sha512-dBYMhplGfspKaCnVk9TUy1cZnknWubpuPNEputjz0YJk1G/92R45rn45BvbPMPMtC5LVcIdxJGPOaOSQTiuzJw==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - jest-mock@30.4.1: - resolution: {integrity: sha512-/i8SVb8/NSB7RfNi8gfqu8gxLV23KaL5EpAttyb9iz8qWRIqXRLflycz/32wXsYkOnaUlx8NAKnJYtpsmXUmfw==} + jest-mock@30.5.0: + resolution: {integrity: sha512-bP5MHZpkYrV7xpV+yvhl36DPcXoEmTR57Un5EACcdVpMY7mpkDefCBq+V4mhcjE/3rwUajT6OTrcJTN7EwN1BA==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - jest-pnp-resolver@1.2.3: - resolution: {integrity: sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==} - engines: {node: '>=6'} - peerDependencies: - jest-resolve: '*' - peerDependenciesMeta: - jest-resolve: - optional: true - - jest-regex-util@30.4.0: - resolution: {integrity: sha512-mWlvLviKIgIQ8VCuM1xRdD0TWp3zlzionlmDBjuXVBs+VkmXq6FgW9T4Emr7oGz/Rk6feDCGyiugolcQEyp3mg==} + jest-regex-util@30.5.0: + resolution: {integrity: sha512-Mg0WK7A6xRHLSA1udJ8y9f3lM0uUhFTBnLKzwPmqB9AylvpleJ6BLemR8K9dK27DY+cesDryoA7yLZCAHsPG1A==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - jest-resolve-dependencies@30.4.2: - resolution: {integrity: sha512-gDiVh1I+GxYzz9oXlyw+1wv6VOYX1WYxMOfjsA3iGKePV2oxmbHhwxfkALxNxYy1ciw6APWwkW2zZONwP97aEQ==} + jest-resolve-dependencies@30.5.0: + resolution: {integrity: sha512-TnSBAp3wGOnqXBmLT3OXtJkugw6Vj2KU0IPde2EJmbGns/QMoNlkauJ7jZ8KYaxONSpx0o4pUvi+u1Q/LSk3Pg==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - jest-resolve@30.4.1: - resolution: {integrity: sha512-Zry8Yq/yJcNAZ7dJ5F2heic8AheXvbFZ7XI5V+h28nrYZ7Qoyy4dItq8OodjnYD270mvX+ZudmrNV9cysqhW5Q==} + jest-resolve@30.5.0: + resolution: {integrity: sha512-NFvQWJ4G7e2kN5712iG+12Xr325NRFGAIhovrwLfgq5Oqd4bFHITw4CzcFkZGp1GTCqemC4v1l8/yZzedKZkjw==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - jest-runner@30.4.2: - resolution: {integrity: sha512-2dw0PslVYXxffXGpLo+Ejad+KcI1Qkjn7f4X4619gf21oCUmL+SPfjqIa/losUem3yEOvfNZe/F1HWUcNpODcg==} + jest-runner@30.5.0: + resolution: {integrity: sha512-Q6Yt+1LvXvEstvru6sQLT7OQYC77VNl6dK0KEvBkeOHgThmXDqNp3Ox9TglDWFHU85pemqTNdKwxfnmO3vgrdA==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - jest-runtime@30.4.2: - resolution: {integrity: sha512-3/5e8iPz2k/VLqlr8DgTftYyLUv8Su3FkCAO2/Od81UsUTpSxOrS6O5x5KkoQwyUjmpYyDJKeyAvg2T2nvpNkQ==} + jest-runtime@30.5.0: + resolution: {integrity: sha512-VTRz0sRIw2EISeHigx1O+CMuwoG4+RKJjF8dp8okzFaDNQEcv5Mw0h5QW8VJXgb2CRF4gZIjSEBb2jdzXPagFQ==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - jest-snapshot@30.4.1: - resolution: {integrity: sha512-tEOkkfOMppUyeiHwjZswOQ3lcnoTnws/q5FnGIaeIh/jmoU0ZlgMYRR8sTlTj+nNGCoJ0RDq6SfxGxCsyMTPmw==} + jest-snapshot@30.5.0: + resolution: {integrity: sha512-pZWETdcqmKve9MDTE/AX6RaeAbRhzKhhAXGozAW8Pg2FfOSdUrQ/7D+VdwE3fLQsqjpITXJVQvYVZoMEzrUl0Q==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - jest-util@30.4.1: - resolution: {integrity: sha512-vjQb1sACEiv13DKJMDToJpzVW0joCsIQrmbg0fi7CyOOt+g9jTuQl2A216pWRBYhOVt53XbL/2LbMKg1BECWOw==} + jest-util@30.5.0: + resolution: {integrity: sha512-lzU4aGUWaS+2X/B0CmgheDasfnsVlRfZh/rNQxB9b9s8cSYUq5BcqdQA95ld+KqJXBUVVt1sqnMQ2T3OxIalmg==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - jest-validate@30.4.1: - resolution: {integrity: sha512-PDWi4SOwLnwqNDfHZjOcsEFyZ4fc/2W2gVL3DEoyqnB6jCQMLRtfBong8s6omIw3lI0HWOus12xfnFmQtjW3fw==} + jest-validate@30.5.0: + resolution: {integrity: sha512-N/hsPYKgBSzBeVZ2RHCs3yvBbTZNPX7Be8q33zhyo/yeFneBL1swzly31LYU4LJ3zJM9e8TxSM8IYLo2SDJZYQ==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - jest-watcher@30.4.1: - resolution: {integrity: sha512-/l9UonmvCwjHH7d2h3iAwIloLc1H0S8mJZ/LNK3i86hqwPAz8otUJjP9MfYtz9Tt77Su5FD2xGjZn8d31IZHlw==} + jest-watcher@30.5.0: + resolution: {integrity: sha512-ujjnEoL4Uu+Swu3WRwYenWMB9JMEiD2T3OypnveMJ64S/1r/5G8eC4OQ1wEOgYWQqMi+mT+buzccflCHr/XJ9Q==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} jest-worker@27.5.1: resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} engines: {node: '>= 10.13.0'} - jest-worker@30.4.1: - resolution: {integrity: sha512-SHynN/q/QD++iNyvMdy+WMmbCGk8jIsNcRxycXbWubSOhvo6T+j2afcfUSl+3hYsiBebOTo0cT7c2H7CXugu1g==} + jest-worker@30.5.0: + resolution: {integrity: sha512-7kFk/607EoynNHLJa20daivkElM+c9PrCLduYy6AlMkYrXbh5TmVtW1BLXE05Y8baAFsJHMoC3xs2QRRTotwLw==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - jest@30.4.2: - resolution: {integrity: sha512-Yi1jqNC/Oq0N4hBgNH/YvBpP1P57QqundgytzYqy3yqAa7NZPNjSoi4SGbRAXDMdBzNE6xBCi5U7RgfrvMEUVQ==} + jest@30.5.0: + resolution: {integrity: sha512-HeFeOUEKh5gjnp1rjuSCse8Dhj0Y3KA8lsZ3azr4Wnq1nCxwMBu35MDc9mp8iblZxmeAz6wV4P241tyUB7J2Ew==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} hasBin: true peerDependencies: @@ -3950,12 +3760,12 @@ packages: js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - js-yaml@3.15.0: - resolution: {integrity: sha512-ttBQIIQPDeLjpPOohtUdXuXUVoA2uIB6fEH9HyJ7234s5mBJ5wTx20njxplLZQgLaOfpmPQA7X2t5AX6tIPbog==} + js-yaml@3.15.2: + resolution: {integrity: sha512-6EuL879VkRA+1Cz578mKMiKvjPNEuk6+r1JaFzoSWejZmtf7xWbIyw1e3KkxlkzTIt9Taw6JBhEppG7utc1P+w==} hasBin: true - js-yaml@4.3.0: - resolution: {integrity: sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==} + js-yaml@4.3.2: + resolution: {integrity: sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==} hasBin: true js-yaml@5.2.2: @@ -3985,10 +3795,6 @@ packages: json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} - json5@1.0.2: - resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} - hasBin: true - json5@2.2.3: resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} engines: {node: '>=6'} @@ -4033,8 +3839,8 @@ packages: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} - libphonenumber-js@1.13.8: - resolution: {integrity: sha512-80xal1m93rADejw2pMp2MSzFhHCPLEspjHxnH2UtqI+DgAmElsbmLMiqk9niwH9NWAfjsRtaJI+qBrOEmRx9nQ==} + libphonenumber-js@1.13.12: + resolution: {integrity: sha512-uLVeV1c9OTk6qkdqnj+mpMD+ZdnZ0szVyWu58HwMmpwkHA1gCEkyjd3veZQXDnuw9KEwSRjcc9B1pS9XKIN1fA==} lightningcss-android-arm64@1.32.0: resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} @@ -4168,9 +3974,9 @@ packages: lodash@4.18.1: resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==} - log-symbols@4.1.0: - resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} - engines: {node: '>=10'} + log-symbols@7.0.1: + resolution: {integrity: sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg==} + engines: {node: '>=18'} long@5.3.2: resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} @@ -4178,8 +3984,8 @@ packages: lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} - lru-cache@11.5.1: - resolution: {integrity: sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==} + lru-cache@11.5.2: + resolution: {integrity: sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==} engines: {node: 20 || >=22} lru-cache@5.1.1: @@ -4189,17 +3995,17 @@ packages: resolution: {integrity: sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA==} engines: {bun: '>=1.0.0', deno: '>=1.30.0', node: '>=8.0.0'} - lucide-react@1.28.0: - resolution: {integrity: sha512-fARAFJULsGuDDydjp6+6blekG/sBIM29TerzLjc9bQUKAcEfrSc4ZQKb25KRz4OMKd87cZTb5dgq0w/T6KufVg==} + lucide-react@1.38.0: + resolution: {integrity: sha512-xZCyBd/wiVUDactoCc+42TjL0aB7EBOXsuX+tjz+W/sGzw2KhHpL1NOH3FIaVUcpimvUBpIYfz34Ofj9S5JEzQ==} peerDependencies: react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 - magic-string@0.30.17: - resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} - magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + magic-string@1.0.0: + resolution: {integrity: sha512-CGvjzMN08iv6w1mm4/x3Gh1hLb4VnyRUA15FFpl6CsCIGGoe36k7kY5KNz9QDbSBN5I/fWHM6ZlIkUTa5xdUEA==} + make-dir@3.1.0: resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==} engines: {node: '>=8'} @@ -4211,9 +4017,6 @@ packages: make-error@1.3.6: resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} - makeerror@1.0.12: - resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==} - math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} @@ -4222,8 +4025,8 @@ packages: resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} engines: {node: '>= 0.6'} - media-typer@1.1.0: - resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} + media-typer@1.1.1: + resolution: {integrity: sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ==} engines: {node: '>= 0.8'} memfs@3.5.3: @@ -4274,12 +4077,16 @@ packages: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} engines: {node: '>=6'} + mimic-function@5.0.1: + resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} + engines: {node: '>=18'} + min-indent@1.0.1: resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} engines: {node: '>=4'} - minimatch@10.2.5: - resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} + minimatch@10.2.6: + resolution: {integrity: sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==} engines: {node: 18 || 20 || >=22} minimatch@3.1.5: @@ -4300,11 +4107,22 @@ packages: resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} engines: {node: '>=16 || 14 >=14.17'} - motion-dom@12.43.0: - resolution: {integrity: sha512-azKON4d9S65PEoFUiQTMTgPheEmzf2QngdRc50AKfJp9Q9mmcBVw22c8eMq9k8kxOFHdL7+WZY7N/5F/lwiDag==} + motion-dom@13.1.1: + resolution: {integrity: sha512-XSf8VYWSB6G/0IY3rWVbyLcxWXtAVHkN1PQE2agTaCv3u8RGvbwu56TyyR/MNzBqqNavEBTZzErcxI1TxBrjcA==} - motion-utils@12.39.0: - resolution: {integrity: sha512-8nadJAJjTtqRkmRF36FoJTrywK9nnFmnPwnSMyxaOCU7GDjN9RTMJIxx9De8ErM+vpPhMccr/6fo5WciyQLnMQ==} + motion-utils@13.0.0: + resolution: {integrity: sha512-7DnN7TmbLcYXcG4RVadXIihWlyuM9afoUww8Y5Agg431kGKiuL2/OMyP4mJ5wLz+pvN3t5ySClLOaVXJ+wekRQ==} + + motion@13.1.1: + resolution: {integrity: sha512-WNZoK6xiF+kkTqkZ5K7FDDh6A8BG4i5Hc7KXtW8gtTxkpJFds+hIOrDaQGKjQj/AE/i4hJqAaUHEqp/Qo02y6Q==} + peerDependencies: + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + react: + optional: true + react-dom: + optional: true ms@2.1.2: resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} @@ -4316,9 +4134,9 @@ packages: resolution: {integrity: sha512-6rdyFg2kLrMh9Jee7/BMPuV9lEAd7lLW2YUpF9/YxR7njyoUwwQ0ZPh3TaIY50Sw6vlyD2HW3wGOkTS4P79xrQ==} engines: {node: '>= 10.16.0'} - mute-stream@2.0.0: - resolution: {integrity: sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==} - engines: {node: ^18.17.0 || >=20.5.0} + mute-stream@3.0.0: + resolution: {integrity: sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw==} + engines: {node: ^20.17.0 || >=22.9.0} mysql2@3.15.3: resolution: {integrity: sha512-FBrGau0IXmuqg4haEZRBfHNWB5mUARw6hNwPDXXGg0XzVJ50mr/9hb267lvpVMnhZ1FON3qNd4Xfcez1rbFwSg==} @@ -4328,8 +4146,8 @@ packages: resolution: {integrity: sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==} engines: {node: '>=8.0.0'} - nanoid@3.3.16: - resolution: {integrity: sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==} + nanoid@3.3.18: + resolution: {integrity: sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true @@ -4341,9 +4159,9 @@ packages: natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} - negotiator@1.0.0: - resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} - engines: {node: '>= 0.6'} + negotiator@1.1.0: + resolution: {integrity: sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg==} + engines: {node: '>=18'} neo-async@2.6.2: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} @@ -4358,8 +4176,8 @@ packages: react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc - next@16.2.12: - resolution: {integrity: sha512-iD59eYQWmbFcEbX7v/acG5DRym9iw1DdaPoD0WTA920naWsE25wShzJW4+UvAs8MK9EC2kBfIH6vtto1H1PHGw==} + next@16.3.3: + resolution: {integrity: sha512-tuRTx1nQ/yVw83cwJBo9F+njGUgMn3UHQycreWHB8XsStvvAh1AthbI8/4IpKnFaF58F+iSiHejYOlMQ/eq83g==} engines: {node: '>=20.9.0'} hasBin: true peerDependencies: @@ -4382,17 +4200,17 @@ packages: node-abort-controller@3.1.1: resolution: {integrity: sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==} + node-addon-api@7.1.1: + resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} + node-domexception@1.0.0: resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} engines: {node: '>=10.5.0'} deprecated: Use your platform's native DOMException instead - node-emoji@1.11.0: - resolution: {integrity: sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A==} - - node-exports-info@1.6.2: - resolution: {integrity: sha512-kXs9Go0cah0qHVV2v389IXQLdLCeE1xfFtjOAF+iobu0OIoG1pje8At2vMHyaPMiPMnG/LWP50twML21eMcAag==} - engines: {node: '>= 0.4'} + node-emoji@2.2.0: + resolution: {integrity: sha512-Z3lTE9pLaJF47NyMhd4ww1yFTAP8YhYI8SleJiHzM46Fgpm5cnNzSl9XfzFNqbaz+VlJrIj3fXQ4DeN1Rjm6cw==} + engines: {node: '>=18'} node-fetch@2.6.11: resolution: {integrity: sha512-4I6pdBY1EthSqDmJkiNk3JIT8cswwR9nfeW/cPdUagJYEQG7R95WRH74wpz7ma8Gh/9dI9FP+OU+0E4FvtA55w==} @@ -4419,8 +4237,8 @@ packages: node-int64@0.4.0: resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} - node-releases@2.0.50: - resolution: {integrity: sha512-J6l92tKHX6w8Jy5nO1Vuc01NoIiRGi/d6qBKVxh+IQ8Cr3b6HbVNfKiF8ZpFKufTwpwxMmce2W3iQZ861ZRyTg==} + node-releases@2.0.54: + resolution: {integrity: sha512-YHs7BmmcsdAI5Ozuf8JZo6PT0mv2GIWC9vMfvUC3dp65M8hn7Ux8CPL+2oBI7juNuj9d0ndhTcznq2ODBps9cQ==} engines: {node: '>=18'} normalize-package-data@2.5.0: @@ -4455,32 +4273,8 @@ packages: resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} engines: {node: '>= 0.4'} - object-keys@1.1.1: - resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} - engines: {node: '>= 0.4'} - - object.assign@4.1.7: - resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==} - engines: {node: '>= 0.4'} - - object.entries@1.1.9: - resolution: {integrity: sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==} - engines: {node: '>= 0.4'} - - object.fromentries@2.0.8: - resolution: {integrity: sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==} - engines: {node: '>= 0.4'} - - object.groupby@1.0.3: - resolution: {integrity: sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==} - engines: {node: '>= 0.4'} - - object.values@1.2.1: - resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} - engines: {node: '>= 0.4'} - - ohash@2.0.11: - resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==} + ohash@2.0.12: + resolution: {integrity: sha512-65S/5gk9YSsaRjcyf7Nfa6h/d3E8/1gslpXfI4W7Dxn/oap8IKRuNT5VXkLQ1YFKIEg4apRY4Pj6aiwFzrDdmw==} on-finished@2.4.1: resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} @@ -4493,6 +4287,10 @@ packages: resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} engines: {node: '>=6'} + onetime@7.0.0: + resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} + engines: {node: '>=18'} + open@7.4.2: resolution: {integrity: sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==} engines: {node: '>=8'} @@ -4501,13 +4299,9 @@ packages: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} - ora@5.4.1: - resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==} - engines: {node: '>=10'} - - own-keys@1.0.1: - resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} - engines: {node: '>= 0.4'} + ora@9.4.1: + resolution: {integrity: sha512-6VlU9MLXbjVQD04AZCMX28hVtA5bUoadvUqO76MUCVA0ilwJbMiHsITRPfyVm6p/BC0Av/BXMujx39WCe1LEqw==} + engines: {node: '>=20'} p-filter@2.1.0: resolution: {integrity: sha512-ZBxxZ5sL2HghephhpGAQdoskxplTwr7ICaehZwLIlfL6acuVgZPm8yBNuRAFBGEqtD/hmUeq9eqLg2ys9Xr/yw==} @@ -4611,15 +4405,15 @@ packages: peerDependencies: pg: '>=8.0' - pg-protocol@1.15.0: - resolution: {integrity: sha512-cq9sECI5s0+uPUXjbz8ioyPJni6RzsRib0US67i5IoTZKw8fNeYlVE7u8F4dG7vEJJtc5wdD1K189lCCUwqWTQ==} + pg-protocol@1.16.0: + resolution: {integrity: sha512-sILXutLVjCLjcDuOmvhX5e2Z4cS5qG/6Bu3VkpFwdf/633ElGLpEh9bgmuI5I4sqKqkifQiGyiCcx1HdtrK7tg==} pg-types@2.2.0: resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} engines: {node: '>=4'} - pg@8.22.0: - resolution: {integrity: sha512-8wih1vVIBMxoUM2oB4soJsD9tDnDpLv4OXBJ+EJzFsvycD+lfyIreC2gGHq78f8jbLLt+bvlPTFdFZfJkOuzAA==} + pg@8.23.0: + resolution: {integrity: sha512-Ip2EQCngowJLGOfCwkFhPXU7/ljlhn6Rxlmy4XYfL2Y+vyRM59+8uR2xqRWKdYmbXmxCFOAmKxBuSUCdF34qLg==} engines: {node: '>= 16.0.0'} peerDependencies: pg-native: '>=3.0.1' @@ -4637,14 +4431,14 @@ packages: resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} engines: {node: '>=8.6'} - picomatch@4.0.4: - resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} - engines: {node: '>=12'} - picomatch@4.0.5: resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==} engines: {node: '>=12'} + picomatch@4.0.7: + resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==} + engines: {node: '>=12'} + pirates@4.0.7: resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} engines: {node: '>= 6'} @@ -4660,12 +4454,8 @@ packages: resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} engines: {node: '>=4'} - possible-typed-array-names@1.1.0: - resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} - engines: {node: '>= 0.4'} - - postcss@8.5.23: - resolution: {integrity: sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==} + postcss@8.5.26: + resolution: {integrity: sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==} engines: {node: ^10 || ^12 || >=14} postgres-array@2.0.0: @@ -4756,16 +4546,16 @@ packages: engines: {node: '>=14'} hasBin: true - pretty-format@30.4.1: - resolution: {integrity: sha512-K6KiKMHTL4jjX4u3Kir2EW07nRfcqVTXIImx50wbjHQTcZPgg+gjVeNTIT3l3L1Rd4UefxfogquC9J37SoFyyw==} + pretty-format@30.5.0: + resolution: {integrity: sha512-mzNzBErpHwM0zpmWS7ExOv62yhQhvd546nUuFqVR0dmnJB59tfrw9sjDF0DJknwsr59OXP0buwJ7PaKguczHSg==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} prisma-dbml-generator@0.12.0: resolution: {integrity: sha512-b5CqA9cCY5jmNJGjx8oUbPw3KSgVd+mu8711MSZIRY9dIF0Vlcs/Au6LR3S5guncydM7Zkh8iz8vC+c2CDj3Xw==} hasBin: true - prisma@7.9.1: - resolution: {integrity: sha512-aPqePoZIqwlAchbgbFDO/wHqGB+7H1nj9gaM+OsL9h77S5S3TnLd9BgD3LnoeDikULo7cl2HSUrEyQ55Z7DYbg==} + prisma@7.10.0: + resolution: {integrity: sha512-o0ornyJOWgygVAzGCpr8PdXV8EJLHyVGDDUr/voBQt8Azzw8cYTByzzPGcA/m4tCkPcnJA8raEOv2CslsKhPEw==} engines: {node: ^20.19 || ^22.12 || >=24.0} hasBin: true peerDependencies: @@ -4813,8 +4603,8 @@ packages: pure-rand@7.0.1: resolution: {integrity: sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==} - qs@6.15.3: - resolution: {integrity: sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==} + qs@6.16.0: + resolution: {integrity: sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA==} engines: {node: '>=0.6'} queue-microtask@1.2.3: @@ -4879,29 +4669,17 @@ packages: resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} engines: {node: '>= 14.18.0'} - readdirp@5.0.0: - resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==} + readdirp@5.1.1: + resolution: {integrity: sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA==} engines: {node: '>= 20.19.0'} redis-errors@1.2.0: resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==} engines: {node: '>=4'} - redis-parser@3.0.0: - resolution: {integrity: sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==} - engines: {node: '>=4'} - reflect-metadata@0.2.2: resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==} - reflect.getprototypeof@1.0.10: - resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} - engines: {node: '>= 0.4'} - - regexp.prototype.flags@1.5.4: - resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} - engines: {node: '>= 0.4'} - remeda@2.33.4: resolution: {integrity: sha512-ygHswjlc/opg2VrtiYvUOPLjxjtdKvjGz1/plDhkG66hjNjFr1xmfrs2ClNFo/E6TyUFiwYNh53bKV26oBoMGQ==} @@ -4933,14 +4711,9 @@ packages: resolution: {integrity: sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==} hasBin: true - resolve@2.0.0-next.7: - resolution: {integrity: sha512-tqt+NBWwyaMgw3zDsnygx4CByWjQEJHOPMdslYhppaQSJUtL/D4JO9CcBBlhPoI8lz9oJIDXkwXfhF4aWqP8xQ==} - engines: {node: '>= 0.4'} - hasBin: true - - restore-cursor@3.1.0: - resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} - engines: {node: '>=8'} + restore-cursor@5.1.0: + resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} + engines: {node: '>=18'} ret@0.5.0: resolution: {integrity: sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==} @@ -4977,30 +4750,15 @@ packages: run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} - rxjs@7.8.1: - resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==} - rxjs@7.8.2: resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} - safe-array-concat@1.1.4: - resolution: {integrity: sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg==} - engines: {node: '>=0.4'} - safe-buffer@5.1.2: resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} - safe-push-apply@1.0.0: - resolution: {integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==} - engines: {node: '>= 0.4'} - - safe-regex-test@1.1.0: - resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} - engines: {node: '>= 0.4'} - safe-regex2@5.1.1: resolution: {integrity: sha512-mOSBvHGDZMuIEZMdOz/aCEYDCv0E7nfcNsIhUF+/P+xC7Hyf3FkvymqgPbg9D1EdSGu+uKbJgy09K/RKKc7kJA==} hasBin: true @@ -5046,23 +4804,11 @@ packages: server-only@0.0.1: resolution: {integrity: sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==} - set-function-length@1.2.2: - resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} - engines: {node: '>= 0.4'} - - set-function-name@2.0.2: - resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} - engines: {node: '>= 0.4'} - - set-proto@1.0.0: - resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==} - engines: {node: '>= 0.4'} - setprototypeof@1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} - sharp@0.35.3: - resolution: {integrity: sha512-ej0zVHuZGHCiABXcNxeYhpRnPNPAcvbG8RMdBAhDAxLKkCRVSpK3Iyu7qbqw3JMzoj0REeM6f3tJLtVwl0023Q==} + sharp@0.35.4: + resolution: {integrity: sha512-n++8XWcj+jCOr2IOl7h8LbKnGBDY4aPbmprMONBNFdn0ImXqpGVv5zliDs0V9HbmbCQLpbuo2ej9rAoOQTvMDA==} engines: {node: '>=20.9.0'} peerDependencies: '@types/node': '*' @@ -5104,6 +4850,10 @@ packages: sisteransi@1.0.5: resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + skin-tone@2.0.0: + resolution: {integrity: sha512-kUMbT1oBJCpgrnKoSr0o6wPtvRWT9W9UKvGLwfJYO2WuahZRHOpEyL1ckyMGgMWh0UdpmaoFqKKD29WTomNEGA==} + engines: {node: '>=8'} + slash@3.0.0: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} @@ -5116,9 +4866,6 @@ packages: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} - source-map-support@0.5.13: - resolution: {integrity: sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==} - source-map-support@0.5.21: resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} @@ -5126,9 +4873,9 @@ packages: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} - source-map@0.7.4: - resolution: {integrity: sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==} - engines: {node: '>= 8'} + source-map@0.7.6: + resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} + engines: {node: '>= 12'} spdx-correct@3.2.0: resolution: {integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==} @@ -5167,9 +4914,9 @@ packages: std-env@3.10.0: resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} - stop-iteration-iterator@1.1.0: - resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} - engines: {node: '>= 0.4'} + stdin-discarder@0.3.2: + resolution: {integrity: sha512-eCPu1qRxPVkl5605OTWF8Wz40b4Mf45NY5LQmVPQ599knfs5QhASUm9GbJ5BDMDOXgrnh0wyEdvzmL//YMlw0A==} + engines: {node: '>=18'} stream-browserify@3.0.0: resolution: {integrity: sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA==} @@ -5178,8 +4925,8 @@ packages: resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} engines: {node: '>=10.0.0'} - streamx@2.28.0: - resolution: {integrity: sha512-1Yowhzjf0ivGMrTIkY9hav5TxobO9qIVqUE41fiCGMGgc3CLlf4MY+9AHmZqBWgDTue0fY9zWjYFVyf6Diuobw==} + streamx@2.28.1: + resolution: {integrity: sha512-zEzXb0s5Cds7tqMH6rhZ05lcJydCWiQPEwiNngVqzsxCc962vLY4Uw+mW7od8kDH258k2Uz/JrOkdIAAhSh9VA==} string-length@4.0.2: resolution: {integrity: sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==} @@ -5193,17 +4940,9 @@ packages: resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} engines: {node: '>=12'} - string.prototype.trim@1.2.11: - resolution: {integrity: sha512-PwvK7BU+CMTJGYQCTZb5RWXIML92lftJLhQz1tBzgKiqGxJaMlBAa48POXaNAC2s4y8jr3EFqrkF9+44neS46w==} - engines: {node: '>= 0.4'} - - string.prototype.trimend@1.0.10: - resolution: {integrity: sha512-2+3aDAOmPTmuFwjDnmJG2ctEkQKVki7vOSqaxkv42Mowj1V6PnvuwFCRrR5lChUux1TBskPjfkeTOhqczDMxTw==} - engines: {node: '>= 0.4'} - - string.prototype.trimstart@1.0.8: - resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} - engines: {node: '>= 0.4'} + string-width@8.2.2: + resolution: {integrity: sha512-GaPUh5gfdrYzqeVNZvUfT23vYYxXzKYidUcnMtJg/3rxRV63EFZy3k6xfKlmfeJD0176lnUV/Usr3XcwSvFzpg==} + engines: {node: '>=20'} string_decoder@1.1.1: resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} @@ -5280,12 +5019,8 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} - swagger-ui-dist@5.32.8: - resolution: {integrity: sha512-dgMdWXIgnI4zX4OPhKEdWnlDODbgm8W3AX0Ivn/BBqcUh6xZsBxhZMnvk6DJyRz1BTrj8dPxtarmEGgkz30oyA==} - - symbol-observable@4.0.0: - resolution: {integrity: sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==} - engines: {node: '>=0.10'} + swagger-ui-dist@5.32.14: + resolution: {integrity: sha512-nOA2pSQhcmODMUQZpJHYKNuwniDUqcOWGNaSCOoZv12FdOSJ9JxV95HtyRGNMqEBj6h6lCNTy20TgZDYTSuUIg==} synckit@0.11.13: resolution: {integrity: sha512-eNRKgb3z66Yp3D2CixVujOUvXLFUTij/zVnV8KRyvFdQwpz7I5DS8UfRkTeLzb64u+dkzDSdelE24izu+zSSUg==} @@ -5302,8 +5037,8 @@ packages: resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} engines: {node: '>=6'} - tar-stream@3.2.0: - resolution: {integrity: sha512-ojzvCvVaNp6aOTFmG7jaRD0meowIAuPc3cMMhSgKiVWws1GyHbGd/xvnyuRKcKlMpt3qvxx6r0hreCNITP9hIg==} + tar-stream@3.2.1: + resolution: {integrity: sha512-nqsEO8zLZJvrOMdEwkA0QdCLFbetHMn95Zqu4fKwX+hkaTWJPZZOrxx/PwtxoK0MMGQmBQNRW3CPs8IFYQz4cQ==} teex@1.0.1: resolution: {integrity: sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==} @@ -5371,14 +5106,14 @@ packages: uglify-js: optional: true - terser@5.48.0: - resolution: {integrity: sha512-J/9An6vs9Us6wKRriSFXBWdRZapREHqFzdNUKk0pmu804EMR6dr6winwo7e5JDxN4xahxQsuysyYFwlwj4XN/Q==} + terser@5.51.2: + resolution: {integrity: sha512-bWnjSNscmuI+GJze6ZupnHP8G/cTcsJF+bXCeQknk2SHQsgbNJnLrqiH9jZ2W4STPVXH2mDKKRX3iwPhc9Cn/Q==} engines: {node: '>=10'} hasBin: true - test-exclude@6.0.0: - resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} - engines: {node: '>=8'} + test-exclude@7.0.2: + resolution: {integrity: sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw==} + engines: {node: '>=18'} text-decoder@1.2.7: resolution: {integrity: sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==} @@ -5387,12 +5122,9 @@ packages: resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} engines: {node: '>=12.0.0'} - tmp@0.2.1: - resolution: {integrity: sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==} - engines: {node: '>=8.17.0'} - - tmpl@1.0.5: - resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} + tmp@0.2.7: + resolution: {integrity: sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==} + engines: {node: '>=14.14'} to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} @@ -5463,9 +5195,6 @@ packages: resolution: {integrity: sha512-zbem3rfRS8BgeNK50Zz5SIQgXzLafiHjOwUAvk/38/o1jHn/V5QAgVUcz884or7WYcPaH3N2CIfUc2u0ul7UcA==} engines: {node: '>=10.13.0'} - tsconfig-paths@3.15.0: - resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} - tsconfig-paths@4.2.0: resolution: {integrity: sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==} engines: {node: '>=6'} @@ -5473,8 +5202,8 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} - turbo@2.10.7: - resolution: {integrity: sha512-GHx6WExIFSKNJ5qMlzDpXBXlu9ApxaMjqxAVrCNcW94xf/+uqgIz41SAuRUMbXva2ExNAaY/h8V0q90SWSzmRw==} + turbo@2.10.12: + resolution: {integrity: sha512-AswgMPnpOoaVZHrrSBejETzEbuIA69OVGwfkHwfrY0A23VjWXBANzgq9+OymWOHAIArB7D1+1z498WY8fGg1Jw==} hasBin: true type-check@0.4.0: @@ -5517,30 +5246,9 @@ packages: resolution: {integrity: sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==} engines: {node: '>= 18'} - typed-array-buffer@1.0.3: - resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} - engines: {node: '>= 0.4'} - - typed-array-byte-length@1.0.3: - resolution: {integrity: sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==} - engines: {node: '>= 0.4'} - - typed-array-byte-offset@1.0.4: - resolution: {integrity: sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==} - engines: {node: '>= 0.4'} - - typed-array-length@1.0.8: - resolution: {integrity: sha512-phPGCwqr2+Qo0fwniCE8e4pKnGu/yFb5nD5Y8bf0EEeiI5GklnACYA9GFy/DrAeRrKHXvHn+1SUsOWgJp6RO+g==} - engines: {node: '>= 0.4'} - typedarray@0.0.6: resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} - typescript@5.9.3: - resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} - engines: {node: '>=14.17'} - hasBin: true - typescript@6.0.3: resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} engines: {node: '>=14.17'} @@ -5559,13 +5267,13 @@ packages: resolution: {integrity: sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==} engines: {node: '>=18'} - unbox-primitive@1.1.0: - resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} - engines: {node: '>= 0.4'} - undici-types@8.3.0: resolution: {integrity: sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==} + unicode-emoji-modifier-base@1.0.0: + resolution: {integrity: sha512-yLSH4py7oFH3oG/9K+XWrz1pSi3dfUrWEnInbxMfArOfc1+33BlGPQtLsOYwvdMy11AwUBetYuaRxSPqgkq+8g==} + engines: {node: '>=4'} + unique-string@2.0.0: resolution: {integrity: sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==} engines: {node: '>=8'} @@ -5581,8 +5289,8 @@ packages: unrs-resolver@1.12.2: resolution: {integrity: sha512-dmlRxBJJayXjqTwC+JtF1HhJmgf3ftQ3YejFcZrf4+KKtJv0qDsK1pjqaaVjG7wJ5NJ6UVP1OqRMQ71Z4C3rxQ==} - update-browserslist-db@1.2.3: - resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + update-browserslist-db@1.3.2: + resolution: {integrity: sha512-UQ+MSxlhRm1bzjhU+DcuXfjFO1FzNtqhK5+9Yvlp90ItDLk5vT932A0rFu619nf7RVS+Y/VeaUW1jaRDqZ8VJw==} hasBin: true peerDependencies: browserslist: '>= 4.21.0' @@ -5632,16 +5340,10 @@ packages: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} - walker@1.0.8: - resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} - watchpack@2.5.2: resolution: {integrity: sha512-6i/00NBjP4yGPs+caKSyRfpTF/8Torsu0MOW3mMzIbhgISFder8i7xbqgHlLMwJrdiN8ndBV3UA1/AfzPSr+jg==} engines: {node: '>=10.13.0'} - wcwidth@1.0.1: - resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} - web-streams-polyfill@3.3.3: resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} engines: {node: '>= 8'} @@ -5653,8 +5355,8 @@ packages: resolution: {integrity: sha512-LnL6Z3GGDPht/AigwRh2dvL9PQPFQ8skEpVrWZXLWBYmqcaojHNN0onvHzie6rq7EWKrrBfPYqNEzTJgiwEQDQ==} engines: {node: '>=6'} - webpack-sources@3.5.0: - resolution: {integrity: sha512-HPuy+uuoTCaaoEoI1LQ3JN9+vrPBvEesnnX1jADHy728cHSMlq4wUc4afYqahq2B1mhQVZxCXOkNTnXltr+2vQ==} + webpack-sources@3.5.1: + resolution: {integrity: sha512-jyuiGJdtvY434z5bUZrjz67v76/ePNvFZTp9Mdz29IlH4+GPsgyGjiv0fKI+M7BdkU6ADjulUcKAd3tUK3WlEw==} engines: {node: '>=10.13.0'} webpack@5.106.2: @@ -5670,22 +5372,6 @@ packages: whatwg-url@5.0.0: resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} - which-boxed-primitive@1.1.1: - resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} - engines: {node: '>= 0.4'} - - which-builtin-type@1.2.1: - resolution: {integrity: sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==} - engines: {node: '>= 0.4'} - - which-collection@1.0.2: - resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} - engines: {node: '>= 0.4'} - - which-typed-array@1.1.22: - resolution: {integrity: sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw==} - engines: {node: '>= 0.4'} - which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -5702,10 +5388,6 @@ packages: wordwrap@1.0.0: resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} - wrap-ansi@6.2.0: - resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} - engines: {node: '>=8'} - wrap-ansi@7.0.0: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} @@ -5748,8 +5430,8 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} - yoctocolors-cjs@2.1.3: - resolution: {integrity: sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==} + yoctocolors@2.2.0: + resolution: {integrity: sha512-xYqdZFUK/VYazNl/oCDYN+3WloWQwMfZxBoiNt6qNyk+xfOdi598muWE42rNZFp1kNOiqW936q5RhUdnpqElSg==} engines: {node: '>=18'} zeptomatch@2.1.0: @@ -5767,233 +5449,209 @@ snapshots: '@alloc/quick-lru@5.2.0': {} - '@angular-devkit/core@19.2.24(chokidar@4.0.3)': - dependencies: - ajv: 8.18.0 - ajv-formats: 3.0.1(ajv@8.18.0) - jsonc-parser: 3.3.1 - picomatch: 4.0.4 - rxjs: 7.8.1 - source-map: 0.7.4 - optionalDependencies: - chokidar: 4.0.3 - - '@angular-devkit/core@19.2.27(chokidar@4.0.3)': + '@angular-devkit/core@22.1.5(chokidar@5.0.0)': dependencies: - ajv: 8.18.0 - ajv-formats: 3.0.1(ajv@8.18.0) + ajv: 8.20.0 + ajv-formats: 3.0.1(ajv@8.20.0) jsonc-parser: 3.3.1 - picomatch: 4.0.4 - rxjs: 7.8.1 - source-map: 0.7.4 + picomatch: 4.0.5 + rxjs: 7.8.2 + source-map: 0.7.6 optionalDependencies: - chokidar: 4.0.3 + chokidar: 5.0.0 - '@angular-devkit/schematics-cli@19.2.27(@types/node@26.1.2)(chokidar@4.0.3)': + '@angular-devkit/schematics-cli@22.1.5(@types/node@26.4.0)(chokidar@5.0.0)': dependencies: - '@angular-devkit/core': 19.2.27(chokidar@4.0.3) - '@angular-devkit/schematics': 19.2.27(chokidar@4.0.3) - '@inquirer/prompts': 7.3.2(@types/node@26.1.2) - ansi-colors: 4.1.3 - symbol-observable: 4.0.0 - yargs-parser: 21.1.1 + '@angular-devkit/core': 22.1.5(chokidar@5.0.0) + '@angular-devkit/schematics': 22.1.5(chokidar@5.0.0) + '@inquirer/prompts': 8.5.2(@types/node@26.4.0) transitivePeerDependencies: - '@types/node' - chokidar - '@angular-devkit/schematics@19.2.24(chokidar@4.0.3)': - dependencies: - '@angular-devkit/core': 19.2.24(chokidar@4.0.3) - jsonc-parser: 3.3.1 - magic-string: 0.30.17 - ora: 5.4.1 - rxjs: 7.8.1 - transitivePeerDependencies: - - chokidar - - '@angular-devkit/schematics@19.2.27(chokidar@4.0.3)': + '@angular-devkit/schematics@22.1.5(chokidar@5.0.0)': dependencies: - '@angular-devkit/core': 19.2.27(chokidar@4.0.3) + '@angular-devkit/core': 22.1.5(chokidar@5.0.0) jsonc-parser: 3.3.1 - magic-string: 0.30.17 - ora: 5.4.1 - rxjs: 7.8.1 + magic-string: 1.0.0 + ora: 9.4.1 + rxjs: 7.8.2 transitivePeerDependencies: - chokidar '@antfu/ni@0.21.4': {} - '@aws-sdk/checksums@3.1000.23': + '@aws-sdk/checksums@3.1000.29': dependencies: - '@aws-sdk/core': 3.977.3 - '@aws-sdk/types': 3.974.2 - '@smithy/core': 3.31.1 - '@smithy/types': 4.16.1 + '@aws-sdk/core': 3.977.9 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.33.3 + '@smithy/types': 4.17.2 tslib: 2.8.1 - '@aws-sdk/client-s3@3.1098.0': - dependencies: - '@aws-sdk/checksums': 3.1000.23 - '@aws-sdk/core': 3.977.3 - '@aws-sdk/credential-provider-node': 3.972.75 - '@aws-sdk/middleware-sdk-s3': 3.972.69 - '@aws-sdk/signature-v4-multi-region': 3.996.43 - '@aws-sdk/types': 3.974.2 - '@smithy/core': 3.31.1 - '@smithy/fetch-http-handler': 5.6.13 - '@smithy/node-http-handler': 4.9.13 - '@smithy/types': 4.16.1 + '@aws-sdk/client-s3@3.1121.0': + dependencies: + '@aws-sdk/checksums': 3.1000.29 + '@aws-sdk/core': 3.977.9 + '@aws-sdk/credential-provider-node': 3.972.81 + '@aws-sdk/middleware-sdk-s3': 3.972.75 + '@aws-sdk/signature-v4-multi-region': 3.996.46 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.33.3 + '@smithy/fetch-http-handler': 5.7.2 + '@smithy/node-http-handler': 4.11.3 + '@smithy/types': 4.17.2 tslib: 2.8.1 - '@aws-sdk/core@3.977.3': + '@aws-sdk/core@3.977.9': dependencies: - '@aws-sdk/types': 3.974.2 - '@aws-sdk/xml-builder': 3.972.37 + '@aws-sdk/types': 3.974.5 + '@aws-sdk/xml-builder': 3.972.40 '@aws/lambda-invoke-store': 0.3.0 - '@smithy/core': 3.31.1 - '@smithy/signature-v4': 5.6.12 - '@smithy/types': 4.16.1 + '@smithy/core': 3.33.3 + '@smithy/signature-v4': 5.7.3 + '@smithy/types': 4.17.2 bowser: 2.14.1 tslib: 2.8.1 - '@aws-sdk/credential-provider-env@3.972.64': + '@aws-sdk/credential-provider-env@3.972.70': dependencies: - '@aws-sdk/core': 3.977.3 - '@aws-sdk/types': 3.974.2 - '@smithy/core': 3.31.1 - '@smithy/types': 4.16.1 + '@aws-sdk/core': 3.977.9 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.33.3 + '@smithy/types': 4.17.2 tslib: 2.8.1 - '@aws-sdk/credential-provider-http@3.972.66': + '@aws-sdk/credential-provider-http@3.972.72': dependencies: - '@aws-sdk/core': 3.977.3 - '@aws-sdk/types': 3.974.2 - '@smithy/core': 3.31.1 - '@smithy/fetch-http-handler': 5.6.13 - '@smithy/node-http-handler': 4.9.13 - '@smithy/types': 4.16.1 + '@aws-sdk/core': 3.977.9 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.33.3 + '@smithy/fetch-http-handler': 5.7.2 + '@smithy/node-http-handler': 4.11.3 + '@smithy/types': 4.17.2 tslib: 2.8.1 - '@aws-sdk/credential-provider-ini@3.973.9': - dependencies: - '@aws-sdk/core': 3.977.3 - '@aws-sdk/credential-provider-env': 3.972.64 - '@aws-sdk/credential-provider-http': 3.972.66 - '@aws-sdk/credential-provider-login': 3.972.71 - '@aws-sdk/credential-provider-process': 3.972.64 - '@aws-sdk/credential-provider-sso': 3.973.8 - '@aws-sdk/credential-provider-web-identity': 3.972.70 - '@aws-sdk/nested-clients': 3.997.38 - '@aws-sdk/types': 3.974.2 - '@smithy/core': 3.31.1 - '@smithy/credential-provider-imds': 4.4.16 - '@smithy/types': 4.16.1 + '@aws-sdk/credential-provider-ini@3.973.15': + dependencies: + '@aws-sdk/core': 3.977.9 + '@aws-sdk/credential-provider-env': 3.972.70 + '@aws-sdk/credential-provider-http': 3.972.72 + '@aws-sdk/credential-provider-login': 3.972.77 + '@aws-sdk/credential-provider-process': 3.972.70 + '@aws-sdk/credential-provider-sso': 3.973.14 + '@aws-sdk/credential-provider-web-identity': 3.972.76 + '@aws-sdk/nested-clients': 3.997.44 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.33.3 + '@smithy/credential-provider-imds': 4.5.2 + '@smithy/types': 4.17.2 tslib: 2.8.1 - '@aws-sdk/credential-provider-login@3.972.71': + '@aws-sdk/credential-provider-login@3.972.77': dependencies: - '@aws-sdk/core': 3.977.3 - '@aws-sdk/nested-clients': 3.997.38 - '@aws-sdk/types': 3.974.2 - '@smithy/core': 3.31.1 - '@smithy/types': 4.16.1 + '@aws-sdk/core': 3.977.9 + '@aws-sdk/nested-clients': 3.997.44 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.33.3 + '@smithy/types': 4.17.2 tslib: 2.8.1 - '@aws-sdk/credential-provider-node@3.972.75': - dependencies: - '@aws-sdk/credential-provider-env': 3.972.64 - '@aws-sdk/credential-provider-http': 3.972.66 - '@aws-sdk/credential-provider-ini': 3.973.9 - '@aws-sdk/credential-provider-process': 3.972.64 - '@aws-sdk/credential-provider-sso': 3.973.8 - '@aws-sdk/credential-provider-web-identity': 3.972.70 - '@aws-sdk/types': 3.974.2 - '@smithy/core': 3.31.1 - '@smithy/credential-provider-imds': 4.4.16 - '@smithy/types': 4.16.1 + '@aws-sdk/credential-provider-node@3.972.81': + dependencies: + '@aws-sdk/credential-provider-env': 3.972.70 + '@aws-sdk/credential-provider-http': 3.972.72 + '@aws-sdk/credential-provider-ini': 3.973.15 + '@aws-sdk/credential-provider-process': 3.972.70 + '@aws-sdk/credential-provider-sso': 3.973.14 + '@aws-sdk/credential-provider-web-identity': 3.972.76 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.33.3 + '@smithy/credential-provider-imds': 4.5.2 + '@smithy/types': 4.17.2 tslib: 2.8.1 - '@aws-sdk/credential-provider-process@3.972.64': + '@aws-sdk/credential-provider-process@3.972.70': dependencies: - '@aws-sdk/core': 3.977.3 - '@aws-sdk/types': 3.974.2 - '@smithy/core': 3.31.1 - '@smithy/types': 4.16.1 + '@aws-sdk/core': 3.977.9 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.33.3 + '@smithy/types': 4.17.2 tslib: 2.8.1 - '@aws-sdk/credential-provider-sso@3.973.8': + '@aws-sdk/credential-provider-sso@3.973.14': dependencies: - '@aws-sdk/core': 3.977.3 - '@aws-sdk/nested-clients': 3.997.38 - '@aws-sdk/token-providers': 3.1098.0 - '@aws-sdk/types': 3.974.2 - '@smithy/core': 3.31.1 - '@smithy/types': 4.16.1 + '@aws-sdk/core': 3.977.9 + '@aws-sdk/nested-clients': 3.997.44 + '@aws-sdk/token-providers': 3.1116.0 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.33.3 + '@smithy/types': 4.17.2 tslib: 2.8.1 - '@aws-sdk/credential-provider-web-identity@3.972.70': + '@aws-sdk/credential-provider-web-identity@3.972.76': dependencies: - '@aws-sdk/core': 3.977.3 - '@aws-sdk/nested-clients': 3.997.38 - '@aws-sdk/types': 3.974.2 - '@smithy/core': 3.31.1 - '@smithy/types': 4.16.1 + '@aws-sdk/core': 3.977.9 + '@aws-sdk/nested-clients': 3.997.44 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.33.3 + '@smithy/types': 4.17.2 tslib: 2.8.1 - '@aws-sdk/lib-storage@3.1098.0(@aws-sdk/client-s3@3.1098.0)': + '@aws-sdk/lib-storage@3.1121.0(@aws-sdk/client-s3@3.1121.0)': dependencies: - '@aws-sdk/client-s3': 3.1098.0 - '@smithy/core': 3.31.1 - '@smithy/types': 4.16.1 + '@aws-sdk/client-s3': 3.1121.0 + '@smithy/core': 3.33.3 + '@smithy/types': 4.17.2 buffer: 5.6.0 events: 3.3.0 stream-browserify: 3.0.0 tslib: 2.8.1 - '@aws-sdk/middleware-sdk-s3@3.972.69': + '@aws-sdk/middleware-sdk-s3@3.972.75': dependencies: - '@aws-sdk/core': 3.977.3 - '@aws-sdk/signature-v4-multi-region': 3.996.43 - '@aws-sdk/types': 3.974.2 - '@smithy/core': 3.31.1 - '@smithy/types': 4.16.1 + '@aws-sdk/core': 3.977.9 + '@aws-sdk/signature-v4-multi-region': 3.996.46 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.33.3 + '@smithy/types': 4.17.2 tslib: 2.8.1 - '@aws-sdk/nested-clients@3.997.38': + '@aws-sdk/nested-clients@3.997.44': dependencies: - '@aws-sdk/core': 3.977.3 - '@aws-sdk/signature-v4-multi-region': 3.996.43 - '@aws-sdk/types': 3.974.2 - '@smithy/core': 3.31.1 - '@smithy/fetch-http-handler': 5.6.13 - '@smithy/node-http-handler': 4.9.13 - '@smithy/types': 4.16.1 + '@aws-sdk/core': 3.977.9 + '@aws-sdk/signature-v4-multi-region': 3.996.46 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.33.3 + '@smithy/fetch-http-handler': 5.7.2 + '@smithy/node-http-handler': 4.11.3 + '@smithy/types': 4.17.2 tslib: 2.8.1 - '@aws-sdk/signature-v4-multi-region@3.996.43': + '@aws-sdk/signature-v4-multi-region@3.996.46': dependencies: - '@aws-sdk/types': 3.974.2 - '@smithy/signature-v4': 5.6.12 - '@smithy/types': 4.16.1 + '@aws-sdk/types': 3.974.5 + '@smithy/signature-v4': 5.7.3 + '@smithy/types': 4.17.2 tslib: 2.8.1 - '@aws-sdk/token-providers@3.1098.0': + '@aws-sdk/token-providers@3.1116.0': dependencies: - '@aws-sdk/core': 3.977.3 - '@aws-sdk/nested-clients': 3.997.38 - '@aws-sdk/types': 3.974.2 - '@smithy/core': 3.31.1 - '@smithy/types': 4.16.1 + '@aws-sdk/core': 3.977.9 + '@aws-sdk/nested-clients': 3.997.44 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.33.3 + '@smithy/types': 4.17.2 tslib: 2.8.1 - '@aws-sdk/types@3.974.2': + '@aws-sdk/types@3.974.5': dependencies: - '@smithy/types': 4.16.1 + '@smithy/types': 4.17.2 tslib: 2.8.1 - '@aws-sdk/xml-builder@3.972.37': + '@aws-sdk/xml-builder@3.972.40': dependencies: - '@smithy/types': 4.16.1 + '@smithy/types': 4.17.2 tslib: 2.8.1 '@aws/lambda-invoke-store@0.3.0': {} @@ -6009,14 +5667,14 @@ snapshots: '@babel/core@7.29.7': dependencies: '@babel/code-frame': 7.29.7 - '@babel/generator': 7.29.7 + '@babel/generator': 7.29.8 '@babel/helper-compilation-targets': 7.29.7 '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7) '@babel/helpers': 7.29.7 - '@babel/parser': 7.29.7 + '@babel/parser': 7.29.8 '@babel/template': 7.29.7 - '@babel/traverse': 7.29.7 - '@babel/types': 7.29.7 + '@babel/traverse': 7.29.8 + '@babel/types': 7.29.8 '@jridgewell/remapping': 2.3.5 convert-source-map: 2.0.0 debug: 4.4.3 @@ -6026,10 +5684,10 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/generator@7.29.7': + '@babel/generator@7.29.8': dependencies: - '@babel/parser': 7.29.7 - '@babel/types': 7.29.7 + '@babel/parser': 7.29.8 + '@babel/types': 7.29.8 '@jridgewell/gen-mapping': 0.3.13 '@jridgewell/trace-mapping': 0.3.31 jsesc: 3.1.0 @@ -6038,7 +5696,7 @@ snapshots: dependencies: '@babel/compat-data': 7.29.7 '@babel/helper-validator-option': 7.29.7 - browserslist: 4.28.4 + browserslist: 4.28.8 lru-cache: 5.1.1 semver: 6.3.1 @@ -6046,8 +5704,8 @@ snapshots: '@babel/helper-module-imports@7.29.7': dependencies: - '@babel/traverse': 7.29.7 - '@babel/types': 7.29.7 + '@babel/traverse': 7.29.8 + '@babel/types': 7.29.8 transitivePeerDependencies: - supports-color @@ -6056,7 +5714,7 @@ snapshots: '@babel/core': 7.29.7 '@babel/helper-module-imports': 7.29.7 '@babel/helper-validator-identifier': 7.29.7 - '@babel/traverse': 7.29.7 + '@babel/traverse': 7.29.8 transitivePeerDependencies: - supports-color @@ -6071,11 +5729,11 @@ snapshots: '@babel/helpers@7.29.7': dependencies: '@babel/template': 7.29.7 - '@babel/types': 7.29.7 + '@babel/types': 7.29.8 - '@babel/parser@7.29.7': + '@babel/parser@7.29.8': dependencies: - '@babel/types': 7.29.7 + '@babel/types': 7.29.8 '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.29.7)': dependencies: @@ -6165,22 +5823,22 @@ snapshots: '@babel/template@7.29.7': dependencies: '@babel/code-frame': 7.29.7 - '@babel/parser': 7.29.7 - '@babel/types': 7.29.7 + '@babel/parser': 7.29.8 + '@babel/types': 7.29.8 - '@babel/traverse@7.29.7': + '@babel/traverse@7.29.8': dependencies: '@babel/code-frame': 7.29.7 - '@babel/generator': 7.29.7 + '@babel/generator': 7.29.8 '@babel/helper-globals': 7.29.7 - '@babel/parser': 7.29.7 + '@babel/parser': 7.29.8 '@babel/template': 7.29.7 - '@babel/types': 7.29.7 + '@babel/types': 7.29.8 debug: 4.4.3 transitivePeerDependencies: - supports-color - '@babel/types@7.29.7': + '@babel/types@7.29.8': dependencies: '@babel/helper-string-parser': 7.29.7 '@babel/helper-validator-identifier': 7.29.7 @@ -6217,7 +5875,7 @@ snapshots: tslib: 2.8.1 optional: true - '@emnapi/runtime@1.11.2': + '@emnapi/runtime@1.11.3': dependencies: tslib: 2.8.1 optional: true @@ -6227,14 +5885,9 @@ snapshots: tslib: 2.8.1 optional: true - '@eslint-community/eslint-utils@4.10.1(eslint@10.8.0(jiti@2.7.0))': - dependencies: - eslint: 10.8.0(jiti@2.7.0) - eslint-visitor-keys: 3.4.3 - - '@eslint-community/eslint-utils@4.9.1(eslint@10.8.0(jiti@2.7.0))': + '@eslint-community/eslint-utils@4.10.1(eslint@10.9.1(jiti@2.7.0))': dependencies: - eslint: 10.8.0(jiti@2.7.0) + eslint: 10.9.1(jiti@2.7.0) eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.2': {} @@ -6243,7 +5896,7 @@ snapshots: dependencies: '@eslint/object-schema': 3.0.5 debug: 4.4.3 - minimatch: 10.2.5 + minimatch: 10.2.6 transitivePeerDependencies: - supports-color @@ -6281,251 +5934,245 @@ snapshots: '@img/colour@1.1.0': optional: true - '@img/sharp-darwin-arm64@0.35.3': + '@img/sharp-darwin-arm64@0.35.4': optionalDependencies: - '@img/sharp-libvips-darwin-arm64': 1.3.2 + '@img/sharp-libvips-darwin-arm64': 1.3.3 optional: true - '@img/sharp-darwin-x64@0.35.3': + '@img/sharp-darwin-x64@0.35.4': optionalDependencies: - '@img/sharp-libvips-darwin-x64': 1.3.2 + '@img/sharp-libvips-darwin-x64': 1.3.3 optional: true - '@img/sharp-freebsd-wasm32@0.35.3': + '@img/sharp-freebsd-wasm32@0.35.4': dependencies: - '@img/sharp-wasm32': 0.35.3 + '@img/sharp-wasm32': 0.35.4 optional: true - '@img/sharp-libvips-darwin-arm64@1.3.2': + '@img/sharp-libvips-darwin-arm64@1.3.3': optional: true - '@img/sharp-libvips-darwin-x64@1.3.2': + '@img/sharp-libvips-darwin-x64@1.3.3': optional: true - '@img/sharp-libvips-linux-arm64@1.3.2': + '@img/sharp-libvips-linux-arm64@1.3.3': optional: true - '@img/sharp-libvips-linux-arm@1.3.2': + '@img/sharp-libvips-linux-arm@1.3.3': optional: true - '@img/sharp-libvips-linux-ppc64@1.3.2': + '@img/sharp-libvips-linux-ppc64@1.3.3': optional: true - '@img/sharp-libvips-linux-riscv64@1.3.2': + '@img/sharp-libvips-linux-riscv64@1.3.3': optional: true - '@img/sharp-libvips-linux-s390x@1.3.2': + '@img/sharp-libvips-linux-s390x@1.3.3': optional: true - '@img/sharp-libvips-linux-x64@1.3.2': + '@img/sharp-libvips-linux-x64@1.3.3': optional: true - '@img/sharp-libvips-linuxmusl-arm64@1.3.2': + '@img/sharp-libvips-linuxmusl-arm64@1.3.3': optional: true - '@img/sharp-libvips-linuxmusl-x64@1.3.2': + '@img/sharp-libvips-linuxmusl-x64@1.3.3': optional: true - '@img/sharp-linux-arm64@0.35.3': + '@img/sharp-linux-arm64@0.35.4': optionalDependencies: - '@img/sharp-libvips-linux-arm64': 1.3.2 + '@img/sharp-libvips-linux-arm64': 1.3.3 optional: true - '@img/sharp-linux-arm@0.35.3': + '@img/sharp-linux-arm@0.35.4': optionalDependencies: - '@img/sharp-libvips-linux-arm': 1.3.2 + '@img/sharp-libvips-linux-arm': 1.3.3 optional: true - '@img/sharp-linux-ppc64@0.35.3': + '@img/sharp-linux-ppc64@0.35.4': optionalDependencies: - '@img/sharp-libvips-linux-ppc64': 1.3.2 + '@img/sharp-libvips-linux-ppc64': 1.3.3 optional: true - '@img/sharp-linux-riscv64@0.35.3': + '@img/sharp-linux-riscv64@0.35.4': optionalDependencies: - '@img/sharp-libvips-linux-riscv64': 1.3.2 + '@img/sharp-libvips-linux-riscv64': 1.3.3 optional: true - '@img/sharp-linux-s390x@0.35.3': + '@img/sharp-linux-s390x@0.35.4': optionalDependencies: - '@img/sharp-libvips-linux-s390x': 1.3.2 + '@img/sharp-libvips-linux-s390x': 1.3.3 optional: true - '@img/sharp-linux-x64@0.35.3': + '@img/sharp-linux-x64@0.35.4': optionalDependencies: - '@img/sharp-libvips-linux-x64': 1.3.2 + '@img/sharp-libvips-linux-x64': 1.3.3 optional: true - '@img/sharp-linuxmusl-arm64@0.35.3': + '@img/sharp-linuxmusl-arm64@0.35.4': optionalDependencies: - '@img/sharp-libvips-linuxmusl-arm64': 1.3.2 + '@img/sharp-libvips-linuxmusl-arm64': 1.3.3 optional: true - '@img/sharp-linuxmusl-x64@0.35.3': + '@img/sharp-linuxmusl-x64@0.35.4': optionalDependencies: - '@img/sharp-libvips-linuxmusl-x64': 1.3.2 + '@img/sharp-libvips-linuxmusl-x64': 1.3.3 optional: true - '@img/sharp-wasm32@0.35.3': + '@img/sharp-wasm32@0.35.4': dependencies: - '@emnapi/runtime': 1.11.2 + '@emnapi/runtime': 1.11.3 optional: true - '@img/sharp-webcontainers-wasm32@0.35.3': + '@img/sharp-webcontainers-wasm32@0.35.4': dependencies: - '@img/sharp-wasm32': 0.35.3 + '@img/sharp-wasm32': 0.35.4 optional: true - '@img/sharp-win32-arm64@0.35.3': + '@img/sharp-win32-arm64@0.35.4': optional: true - '@img/sharp-win32-ia32@0.35.3': + '@img/sharp-win32-ia32@0.35.4': optional: true - '@img/sharp-win32-x64@0.35.3': + '@img/sharp-win32-x64@0.35.4': optional: true - '@inquirer/ansi@1.0.2': {} + '@inquirer/ansi@2.0.7': {} - '@inquirer/checkbox@4.3.2(@types/node@26.1.2)': + '@inquirer/checkbox@5.2.3(@types/node@26.4.0)': dependencies: - '@inquirer/ansi': 1.0.2 - '@inquirer/core': 10.3.2(@types/node@26.1.2) - '@inquirer/figures': 1.0.15 - '@inquirer/type': 3.0.10(@types/node@26.1.2) - yoctocolors-cjs: 2.1.3 + '@inquirer/ansi': 2.0.7 + '@inquirer/core': 12.0.1(@types/node@26.4.0) + '@inquirer/figures': 2.0.8 + '@inquirer/type': 4.1.0(@types/node@26.4.0) optionalDependencies: - '@types/node': 26.1.2 + '@types/node': 26.4.0 - '@inquirer/confirm@5.1.21(@types/node@26.1.2)': + '@inquirer/confirm@6.3.0(@types/node@26.4.0)': dependencies: - '@inquirer/core': 10.3.2(@types/node@26.1.2) - '@inquirer/type': 3.0.10(@types/node@26.1.2) + '@inquirer/core': 12.0.1(@types/node@26.4.0) + '@inquirer/type': 4.1.0(@types/node@26.4.0) optionalDependencies: - '@types/node': 26.1.2 + '@types/node': 26.4.0 - '@inquirer/core@10.3.2(@types/node@26.1.2)': + '@inquirer/core@12.0.1(@types/node@26.4.0)': dependencies: - '@inquirer/ansi': 1.0.2 - '@inquirer/figures': 1.0.15 - '@inquirer/type': 3.0.10(@types/node@26.1.2) + '@inquirer/ansi': 2.0.7 + '@inquirer/figures': 2.0.8 + '@inquirer/type': 4.1.0(@types/node@26.4.0) cli-width: 4.1.0 - mute-stream: 2.0.0 + fast-wrap-ansi: 0.2.2 + mute-stream: 3.0.0 signal-exit: 4.1.0 - wrap-ansi: 6.2.0 - yoctocolors-cjs: 2.1.3 optionalDependencies: - '@types/node': 26.1.2 + '@types/node': 26.4.0 - '@inquirer/editor@4.2.23(@types/node@26.1.2)': + '@inquirer/editor@5.3.1(@types/node@26.4.0)': dependencies: - '@inquirer/core': 10.3.2(@types/node@26.1.2) - '@inquirer/external-editor': 1.0.3(@types/node@26.1.2) - '@inquirer/type': 3.0.10(@types/node@26.1.2) + '@inquirer/core': 12.0.1(@types/node@26.4.0) + '@inquirer/external-editor': 3.0.4(@types/node@26.4.0) + '@inquirer/type': 4.1.0(@types/node@26.4.0) optionalDependencies: - '@types/node': 26.1.2 + '@types/node': 26.4.0 - '@inquirer/expand@4.0.23(@types/node@26.1.2)': + '@inquirer/expand@5.1.3(@types/node@26.4.0)': dependencies: - '@inquirer/core': 10.3.2(@types/node@26.1.2) - '@inquirer/type': 3.0.10(@types/node@26.1.2) - yoctocolors-cjs: 2.1.3 + '@inquirer/core': 12.0.1(@types/node@26.4.0) + '@inquirer/type': 4.1.0(@types/node@26.4.0) optionalDependencies: - '@types/node': 26.1.2 + '@types/node': 26.4.0 - '@inquirer/external-editor@1.0.3(@types/node@26.1.2)': + '@inquirer/external-editor@3.0.4(@types/node@26.4.0)': dependencies: chardet: 2.2.0 iconv-lite: 0.7.3 optionalDependencies: - '@types/node': 26.1.2 + '@types/node': 26.4.0 - '@inquirer/figures@1.0.15': {} + '@inquirer/figures@2.0.8': {} - '@inquirer/input@4.3.1(@types/node@26.1.2)': + '@inquirer/input@5.1.4(@types/node@26.4.0)': dependencies: - '@inquirer/core': 10.3.2(@types/node@26.1.2) - '@inquirer/type': 3.0.10(@types/node@26.1.2) + '@inquirer/core': 12.0.1(@types/node@26.4.0) + '@inquirer/type': 4.1.0(@types/node@26.4.0) optionalDependencies: - '@types/node': 26.1.2 + '@types/node': 26.4.0 - '@inquirer/number@3.0.23(@types/node@26.1.2)': + '@inquirer/number@4.2.1(@types/node@26.4.0)': dependencies: - '@inquirer/core': 10.3.2(@types/node@26.1.2) - '@inquirer/type': 3.0.10(@types/node@26.1.2) + '@inquirer/core': 12.0.1(@types/node@26.4.0) + '@inquirer/type': 4.1.0(@types/node@26.4.0) optionalDependencies: - '@types/node': 26.1.2 + '@types/node': 26.4.0 - '@inquirer/password@4.0.23(@types/node@26.1.2)': + '@inquirer/password@5.2.0(@types/node@26.4.0)': dependencies: - '@inquirer/ansi': 1.0.2 - '@inquirer/core': 10.3.2(@types/node@26.1.2) - '@inquirer/type': 3.0.10(@types/node@26.1.2) + '@inquirer/ansi': 2.0.7 + '@inquirer/core': 12.0.1(@types/node@26.4.0) + '@inquirer/type': 4.1.0(@types/node@26.4.0) optionalDependencies: - '@types/node': 26.1.2 - - '@inquirer/prompts@7.10.1(@types/node@26.1.2)': - dependencies: - '@inquirer/checkbox': 4.3.2(@types/node@26.1.2) - '@inquirer/confirm': 5.1.21(@types/node@26.1.2) - '@inquirer/editor': 4.2.23(@types/node@26.1.2) - '@inquirer/expand': 4.0.23(@types/node@26.1.2) - '@inquirer/input': 4.3.1(@types/node@26.1.2) - '@inquirer/number': 3.0.23(@types/node@26.1.2) - '@inquirer/password': 4.0.23(@types/node@26.1.2) - '@inquirer/rawlist': 4.1.11(@types/node@26.1.2) - '@inquirer/search': 3.2.2(@types/node@26.1.2) - '@inquirer/select': 4.4.2(@types/node@26.1.2) + '@types/node': 26.4.0 + + '@inquirer/prompts@8.5.2(@types/node@26.4.0)': + dependencies: + '@inquirer/checkbox': 5.2.3(@types/node@26.4.0) + '@inquirer/confirm': 6.3.0(@types/node@26.4.0) + '@inquirer/editor': 5.3.1(@types/node@26.4.0) + '@inquirer/expand': 5.1.3(@types/node@26.4.0) + '@inquirer/input': 5.1.4(@types/node@26.4.0) + '@inquirer/number': 4.2.1(@types/node@26.4.0) + '@inquirer/password': 5.2.0(@types/node@26.4.0) + '@inquirer/rawlist': 5.3.3(@types/node@26.4.0) + '@inquirer/search': 4.3.1(@types/node@26.4.0) + '@inquirer/select': 5.2.3(@types/node@26.4.0) optionalDependencies: - '@types/node': 26.1.2 - - '@inquirer/prompts@7.3.2(@types/node@26.1.2)': - dependencies: - '@inquirer/checkbox': 4.3.2(@types/node@26.1.2) - '@inquirer/confirm': 5.1.21(@types/node@26.1.2) - '@inquirer/editor': 4.2.23(@types/node@26.1.2) - '@inquirer/expand': 4.0.23(@types/node@26.1.2) - '@inquirer/input': 4.3.1(@types/node@26.1.2) - '@inquirer/number': 3.0.23(@types/node@26.1.2) - '@inquirer/password': 4.0.23(@types/node@26.1.2) - '@inquirer/rawlist': 4.1.11(@types/node@26.1.2) - '@inquirer/search': 3.2.2(@types/node@26.1.2) - '@inquirer/select': 4.4.2(@types/node@26.1.2) + '@types/node': 26.4.0 + + '@inquirer/prompts@8.7.0(@types/node@26.4.0)': + dependencies: + '@inquirer/checkbox': 5.2.3(@types/node@26.4.0) + '@inquirer/confirm': 6.3.0(@types/node@26.4.0) + '@inquirer/editor': 5.3.1(@types/node@26.4.0) + '@inquirer/expand': 5.1.3(@types/node@26.4.0) + '@inquirer/input': 5.1.4(@types/node@26.4.0) + '@inquirer/number': 4.2.1(@types/node@26.4.0) + '@inquirer/password': 5.2.0(@types/node@26.4.0) + '@inquirer/rawlist': 5.3.3(@types/node@26.4.0) + '@inquirer/search': 4.3.1(@types/node@26.4.0) + '@inquirer/select': 5.2.3(@types/node@26.4.0) optionalDependencies: - '@types/node': 26.1.2 + '@types/node': 26.4.0 - '@inquirer/rawlist@4.1.11(@types/node@26.1.2)': + '@inquirer/rawlist@5.3.3(@types/node@26.4.0)': dependencies: - '@inquirer/core': 10.3.2(@types/node@26.1.2) - '@inquirer/type': 3.0.10(@types/node@26.1.2) - yoctocolors-cjs: 2.1.3 + '@inquirer/core': 12.0.1(@types/node@26.4.0) + '@inquirer/type': 4.1.0(@types/node@26.4.0) optionalDependencies: - '@types/node': 26.1.2 + '@types/node': 26.4.0 - '@inquirer/search@3.2.2(@types/node@26.1.2)': + '@inquirer/search@4.3.1(@types/node@26.4.0)': dependencies: - '@inquirer/core': 10.3.2(@types/node@26.1.2) - '@inquirer/figures': 1.0.15 - '@inquirer/type': 3.0.10(@types/node@26.1.2) - yoctocolors-cjs: 2.1.3 + '@inquirer/core': 12.0.1(@types/node@26.4.0) + '@inquirer/figures': 2.0.8 + '@inquirer/type': 4.1.0(@types/node@26.4.0) optionalDependencies: - '@types/node': 26.1.2 + '@types/node': 26.4.0 - '@inquirer/select@4.4.2(@types/node@26.1.2)': + '@inquirer/select@5.2.3(@types/node@26.4.0)': dependencies: - '@inquirer/ansi': 1.0.2 - '@inquirer/core': 10.3.2(@types/node@26.1.2) - '@inquirer/figures': 1.0.15 - '@inquirer/type': 3.0.10(@types/node@26.1.2) - yoctocolors-cjs: 2.1.3 + '@inquirer/ansi': 2.0.7 + '@inquirer/core': 12.0.1(@types/node@26.4.0) + '@inquirer/figures': 2.0.8 + '@inquirer/type': 4.1.0(@types/node@26.4.0) optionalDependencies: - '@types/node': 26.1.2 + '@types/node': 26.4.0 - '@inquirer/type@3.0.10(@types/node@26.1.2)': + '@inquirer/type@4.1.0(@types/node@26.4.0)': optionalDependencies: - '@types/node': 26.1.2 + '@types/node': 26.4.0 - '@ioredis/commands@1.10.0': {} + '@ioredis/commands@2.0.0': {} '@isaacs/cliui@8.0.2': dependencies: @@ -6541,49 +6188,49 @@ snapshots: camelcase: 5.3.1 find-up: 4.1.0 get-package-type: 0.1.0 - js-yaml: 3.15.0 + js-yaml: 3.15.2 resolve-from: 5.0.0 '@istanbuljs/schema@0.1.6': {} - '@jest/console@30.4.1': + '@jest/console@30.5.0': dependencies: - '@jest/types': 30.4.1 - '@types/node': 26.1.2 + '@jest/types': 30.5.0 + '@types/node': 26.4.0 chalk: 4.1.2 - jest-message-util: 30.4.1 - jest-util: 30.4.1 + jest-message-util: 30.5.0 + jest-util: 30.5.0 slash: 3.0.0 - '@jest/core@30.4.2(ts-node@10.9.2(@types/node@26.1.2)(typescript@6.0.3))': + '@jest/core@30.5.0(ts-node@10.9.2(@types/node@26.4.0)(typescript@6.0.3))': dependencies: - '@jest/console': 30.4.1 - '@jest/pattern': 30.4.0 - '@jest/reporters': 30.4.1 - '@jest/test-result': 30.4.1 - '@jest/transform': 30.4.1 - '@jest/types': 30.4.1 - '@types/node': 26.1.2 + '@jest/console': 30.5.0 + '@jest/pattern': 30.5.0 + '@jest/reporters': 30.5.0 + '@jest/test-result': 30.5.0 + '@jest/transform': 30.5.0 + '@jest/types': 30.5.0 + '@types/node': 26.4.0 ansi-escapes: 4.3.2 chalk: 4.1.2 ci-info: 4.4.0 exit-x: 0.2.2 fast-json-stable-stringify: 2.1.0 graceful-fs: 4.2.11 - jest-changed-files: 30.4.1 - jest-config: 30.4.2(@types/node@26.1.2)(ts-node@10.9.2(@types/node@26.1.2)(typescript@6.0.3)) - jest-haste-map: 30.4.1 - jest-message-util: 30.4.1 - jest-regex-util: 30.4.0 - jest-resolve: 30.4.1 - jest-resolve-dependencies: 30.4.2 - jest-runner: 30.4.2 - jest-runtime: 30.4.2 - jest-snapshot: 30.4.1 - jest-util: 30.4.1 - jest-validate: 30.4.1 - jest-watcher: 30.4.1 - pretty-format: 30.4.1 + jest-changed-files: 30.5.0 + jest-config: 30.5.0(@types/node@26.4.0)(ts-node@10.9.2(@types/node@26.4.0)(typescript@6.0.3)) + jest-haste-map: 30.5.0 + jest-message-util: 30.5.0 + jest-regex-util: 30.5.0 + jest-resolve: 30.5.0 + jest-resolve-dependencies: 30.5.0 + jest-runner: 30.5.0 + jest-runtime: 30.5.0 + jest-snapshot: 30.5.0 + jest-util: 30.5.0 + jest-validate: 30.5.0 + jest-watcher: 30.5.0 + pretty-format: 30.5.0 slash: 3.0.0 transitivePeerDependencies: - babel-plugin-macros @@ -6591,142 +6238,143 @@ snapshots: - supports-color - ts-node - '@jest/diff-sequences@30.4.0': {} + '@jest/diff-sequences@30.5.0': {} - '@jest/environment@30.4.1': + '@jest/environment@30.5.0': dependencies: - '@jest/fake-timers': 30.4.1 - '@jest/types': 30.4.1 - '@types/node': 26.1.2 - jest-mock: 30.4.1 + '@jest/fake-timers': 30.5.0 + '@jest/types': 30.5.0 + '@types/node': 26.4.0 + jest-mock: 30.5.0 - '@jest/expect-utils@30.4.1': + '@jest/expect-utils@30.5.0': dependencies: - '@jest/get-type': 30.1.0 + '@jest/get-type': 30.5.0 - '@jest/expect@30.4.1': + '@jest/expect@30.5.0': dependencies: - expect: 30.4.1 - jest-snapshot: 30.4.1 + expect: 30.5.0 + jest-snapshot: 30.5.0 transitivePeerDependencies: - supports-color - '@jest/fake-timers@30.4.1': + '@jest/fake-timers@30.5.0': dependencies: - '@jest/types': 30.4.1 + '@jest/types': 30.5.0 '@sinonjs/fake-timers': 15.4.0 - '@types/node': 26.1.2 - jest-message-util: 30.4.1 - jest-mock: 30.4.1 - jest-util: 30.4.1 + '@types/node': 26.4.0 + jest-message-util: 30.5.0 + jest-mock: 30.5.0 + jest-util: 30.5.0 - '@jest/get-type@30.1.0': {} + '@jest/get-type@30.5.0': {} - '@jest/globals@30.4.1': + '@jest/globals@30.5.0': dependencies: - '@jest/environment': 30.4.1 - '@jest/expect': 30.4.1 - '@jest/types': 30.4.1 - jest-mock: 30.4.1 + '@jest/environment': 30.5.0 + '@jest/expect': 30.5.0 + '@jest/types': 30.5.0 + jest-mock: 30.5.0 transitivePeerDependencies: - supports-color - '@jest/pattern@30.4.0': + '@jest/pattern@30.5.0': dependencies: - '@types/node': 26.1.2 - jest-regex-util: 30.4.0 + '@types/node': 26.4.0 + jest-regex-util: 30.5.0 - '@jest/reporters@30.4.1': + '@jest/reporters@30.5.0': dependencies: '@bcoe/v8-coverage': 0.2.3 - '@jest/console': 30.4.1 - '@jest/test-result': 30.4.1 - '@jest/transform': 30.4.1 - '@jest/types': 30.4.1 + '@jest/console': 30.5.0 + '@jest/test-result': 30.5.0 + '@jest/transform': 30.5.0 + '@jest/types': 30.5.0 '@jridgewell/trace-mapping': 0.3.31 - '@types/node': 26.1.2 + '@types/node': 26.4.0 chalk: 4.1.2 collect-v8-coverage: 1.0.3 exit-x: 0.2.2 - glob: 10.5.0 + glob: 13.0.6 graceful-fs: 4.2.11 istanbul-lib-coverage: 3.2.2 istanbul-lib-instrument: 6.0.3 istanbul-lib-report: 3.0.1 istanbul-lib-source-maps: 5.0.6 istanbul-reports: 3.2.0 - jest-message-util: 30.4.1 - jest-util: 30.4.1 - jest-worker: 30.4.1 + jest-message-util: 30.5.0 + jest-util: 30.5.0 + jest-worker: 30.5.0 slash: 3.0.0 string-length: 4.0.2 v8-to-istanbul: 9.3.0 transitivePeerDependencies: - supports-color - '@jest/schemas@30.4.1': + '@jest/schemas@30.5.0': dependencies: - '@sinclair/typebox': 0.34.49 + '@sinclair/typebox': 0.34.52 - '@jest/snapshot-utils@30.4.1': + '@jest/snapshot-utils@30.5.0': dependencies: - '@jest/types': 30.4.1 + '@jest/types': 30.5.0 chalk: 4.1.2 graceful-fs: 4.2.11 natural-compare: 1.4.0 - '@jest/source-map@30.0.1': + '@jest/source-map@30.5.0': dependencies: '@jridgewell/trace-mapping': 0.3.31 callsites: 3.1.0 + convert-source-map: 2.0.0 graceful-fs: 4.2.11 - '@jest/test-result@30.4.1': + '@jest/test-result@30.5.0': dependencies: - '@jest/console': 30.4.1 - '@jest/types': 30.4.1 + '@jest/console': 30.5.0 + '@jest/types': 30.5.0 '@types/istanbul-lib-coverage': 2.0.6 collect-v8-coverage: 1.0.3 - '@jest/test-sequencer@30.4.1': + '@jest/test-sequencer@30.5.0': dependencies: - '@jest/test-result': 30.4.1 + '@jest/test-result': 30.5.0 graceful-fs: 4.2.11 - jest-haste-map: 30.4.1 + jest-haste-map: 30.5.0 slash: 3.0.0 - '@jest/transform@30.4.1': + '@jest/transform@30.5.0': dependencies: '@babel/core': 7.29.7 - '@jest/types': 30.4.1 + '@jest/types': 30.5.0 '@jridgewell/trace-mapping': 0.3.31 - babel-plugin-istanbul: 7.0.1 + babel-plugin-istanbul: 8.0.0 chalk: 4.1.2 convert-source-map: 2.0.0 fast-json-stable-stringify: 2.1.0 graceful-fs: 4.2.11 - jest-haste-map: 30.4.1 - jest-regex-util: 30.4.0 - jest-util: 30.4.1 + jest-haste-map: 30.5.0 + jest-regex-util: 30.5.0 + jest-util: 30.5.0 pirates: 4.0.7 slash: 3.0.0 write-file-atomic: 5.0.1 transitivePeerDependencies: - supports-color - '@jest/types@30.4.1': + '@jest/types@30.5.0': dependencies: - '@jest/pattern': 30.4.0 - '@jest/schemas': 30.4.1 + '@jest/pattern': 30.5.0 + '@jest/schemas': 30.5.0 '@types/istanbul-lib-coverage': 2.0.6 '@types/istanbul-reports': 3.0.4 - '@types/node': 26.1.2 + '@types/node': 26.4.0 '@types/yargs': 17.0.35 chalk: 4.1.2 '@jridgewell/gen-mapping@0.3.13': dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/sourcemap-codec': 1.6.0 '@jridgewell/trace-mapping': 0.3.31 '@jridgewell/remapping@2.3.5': @@ -6740,69 +6388,60 @@ snapshots: dependencies: '@jridgewell/gen-mapping': 0.3.13 '@jridgewell/trace-mapping': 0.3.31 + optional: true - '@jridgewell/sourcemap-codec@1.5.5': {} + '@jridgewell/sourcemap-codec@1.6.0': {} '@jridgewell/trace-mapping@0.3.31': dependencies: '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/sourcemap-codec': 1.6.0 '@jridgewell/trace-mapping@0.3.9': dependencies: '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/sourcemap-codec': 1.6.0 '@lukeed/csprng@1.1.0': {} '@microsoft/tsdoc@0.16.0': {} - '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': + '@napi-rs/wasm-runtime@1.2.3(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': dependencies: '@emnapi/core': 1.10.0 '@emnapi/runtime': 1.10.0 '@tybys/wasm-util': 0.10.3 optional: true - '@nestjs/cli@11.0.24(@types/node@26.1.2)(prettier@3.9.6)': + '@nestjs/cli@12.0.0(@types/node@26.4.0)(fork-ts-checker-webpack-plugin@9.1.0(typescript@6.0.3)(webpack@5.106.2))(prettier@3.9.6)(tsconfig-paths-webpack-plugin@4.2.0)(webpack-node-externals@3.0.0)(webpack@5.106.2)': dependencies: - '@angular-devkit/core': 19.2.27(chokidar@4.0.3) - '@angular-devkit/schematics': 19.2.27(chokidar@4.0.3) - '@angular-devkit/schematics-cli': 19.2.27(@types/node@26.1.2)(chokidar@4.0.3) - '@inquirer/prompts': 7.10.1(@types/node@26.1.2) - '@nestjs/schematics': 11.1.0(chokidar@4.0.3)(prettier@3.9.6)(typescript@5.9.3) - ansis: 4.2.0 - chokidar: 4.0.3 + '@angular-devkit/core': 22.1.5(chokidar@5.0.0) + '@angular-devkit/schematics': 22.1.5(chokidar@5.0.0) + '@angular-devkit/schematics-cli': 22.1.5(@types/node@26.4.0)(chokidar@5.0.0) + '@inquirer/prompts': 8.7.0(@types/node@26.4.0) + '@nestjs/schematics': 12.0.0(chokidar@5.0.0)(prettier@3.9.6)(typescript@6.0.3) + ansis: 4.3.1 + chokidar: 5.0.0 cli-table3: 0.6.5 - commander: 4.1.1 - fork-ts-checker-webpack-plugin: 9.1.0(typescript@5.9.3)(webpack@5.106.2) - glob: 13.0.6 - node-emoji: 1.11.0 - ora: 5.4.1 + commander: 15.0.0 + minimatch: 10.2.6 + node-emoji: 2.2.0 + ora: 9.4.1 tsconfig-paths: 4.2.0 + typescript: 6.0.3 + optionalDependencies: + fork-ts-checker-webpack-plugin: 9.1.0(typescript@6.0.3)(webpack@5.106.2) tsconfig-paths-webpack-plugin: 4.2.0 - typescript: 5.9.3 webpack: 5.106.2 webpack-node-externals: 3.0.0 transitivePeerDependencies: - - '@minify-html/node' - - '@swc/css' - - '@swc/html' - '@types/node' - - clean-css - - cssnano - - csso - - esbuild - - html-minifier-terser - - lightningcss - - postcss - prettier - - uglify-js - - webpack-cli - '@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)': + '@nestjs/common@12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)': dependencies: - file-type: 21.3.4 + '@standard-schema/spec': 1.1.0 + file-type: 22.0.2 iterare: 1.2.1 load-esm: 1.0.3 reflect-metadata: 0.2.2 @@ -6815,17 +6454,18 @@ snapshots: transitivePeerDependencies: - supports-color - '@nestjs/config@4.0.4(@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(rxjs@7.8.2)': + '@nestjs/config@12.0.0(@nestjs/common@12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(rxjs@7.8.2)': dependencies: - '@nestjs/common': 11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) - dotenv: 17.4.1 - dotenv-expand: 12.0.3 - lodash: 4.18.1 + '@nestjs/common': 12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@standard-schema/spec': 1.1.0 + dotenv: 17.4.2 + dotenv-expand: 13.0.0 + es-toolkit: 1.51.0 rxjs: 7.8.2 - '@nestjs/core@11.1.28(@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.28)(reflect-metadata@0.2.2)(rxjs@7.8.2)': + '@nestjs/core@12.0.1(@nestjs/common@12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@12.0.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)': dependencies: - '@nestjs/common': 11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) fast-safe-stringify: 2.1.1 iterare: 1.2.1 path-to-regexp: 8.4.2 @@ -6834,26 +6474,26 @@ snapshots: tslib: 2.8.1 uid: 2.0.2 optionalDependencies: - '@nestjs/platform-express': 11.1.28(@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.28) + '@nestjs/platform-express': 12.0.1(@nestjs/common@12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@12.0.1) - '@nestjs/jwt@11.0.2(@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))': + '@nestjs/jwt@12.0.1(@nestjs/common@12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))': dependencies: - '@nestjs/common': 11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@types/jsonwebtoken': 9.0.10 jsonwebtoken: 9.0.3 - '@nestjs/mapped-types@2.1.1(@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)': + '@nestjs/mapped-types@12.0.0(@nestjs/common@12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)': dependencies: - '@nestjs/common': 11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) reflect-metadata: 0.2.2 optionalDependencies: class-transformer: 0.5.1 class-validator: 0.15.1 - '@nestjs/platform-express@11.1.28(@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.28)': + '@nestjs/platform-express@12.0.1(@nestjs/common@12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@12.0.1)': dependencies: - '@nestjs/common': 11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.28(@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.28)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 12.0.1(@nestjs/common@12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@12.0.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) cors: 2.8.6 express: 5.2.1 multer: 2.2.0 @@ -6862,23 +6502,10 @@ snapshots: transitivePeerDependencies: - supports-color - '@nestjs/schematics@11.1.0(chokidar@4.0.3)(prettier@3.9.6)(typescript@5.9.3)': - dependencies: - '@angular-devkit/core': 19.2.24(chokidar@4.0.3) - '@angular-devkit/schematics': 19.2.24(chokidar@4.0.3) - comment-json: 5.0.0 - jsonc-parser: 3.3.1 - pluralize: 8.0.0 - typescript: 5.9.3 - optionalDependencies: - prettier: 3.9.6 - transitivePeerDependencies: - - chokidar - - '@nestjs/schematics@11.1.0(chokidar@4.0.3)(prettier@3.9.6)(typescript@6.0.3)': + '@nestjs/schematics@12.0.0(chokidar@5.0.0)(prettier@3.9.6)(typescript@6.0.3)': dependencies: - '@angular-devkit/core': 19.2.24(chokidar@4.0.3) - '@angular-devkit/schematics': 19.2.24(chokidar@4.0.3) + '@angular-devkit/core': 22.1.5(chokidar@5.0.0) + '@angular-devkit/schematics': 22.1.5(chokidar@5.0.0) comment-json: 5.0.0 jsonc-parser: 3.3.1 pluralize: 8.0.0 @@ -6888,64 +6515,65 @@ snapshots: transitivePeerDependencies: - chokidar - '@nestjs/swagger@11.4.6(@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.28)(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)': + '@nestjs/swagger@12.0.1(@nestjs/common@12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@12.0.1)(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)': dependencies: '@microsoft/tsdoc': 0.16.0 - '@nestjs/common': 11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.28(@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.28)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/mapped-types': 2.1.1(@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2) + '@nestjs/common': 12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 12.0.1(@nestjs/common@12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@12.0.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/mapped-types': 12.0.0(@nestjs/common@12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2) + '@standard-schema/spec': 1.1.0 + es-toolkit: 1.51.0 js-yaml: 5.2.2 - lodash: 4.18.1 path-to-regexp: 8.4.2 reflect-metadata: 0.2.2 - swagger-ui-dist: 5.32.8 + swagger-ui-dist: 5.32.14 optionalDependencies: class-transformer: 0.5.1 class-validator: 0.15.1 - '@nestjs/terminus@11.1.1(@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.28)(@prisma/client@7.9.1(prisma@7.9.1(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@6.0.3))(typescript@6.0.3))(reflect-metadata@0.2.2)(rxjs@7.8.2)': + '@nestjs/terminus@11.1.1(@nestjs/common@12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@12.0.1)(@prisma/client@7.10.0(prisma@7.10.0(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@6.0.3))(typescript@6.0.3))(reflect-metadata@0.2.2)(rxjs@7.8.2)': dependencies: - '@nestjs/common': 11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.28(@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.28)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 12.0.1(@nestjs/common@12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@12.0.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) boxen: 5.1.2 check-disk-space: 3.4.0 reflect-metadata: 0.2.2 rxjs: 7.8.2 optionalDependencies: - '@prisma/client': 7.9.1(prisma@7.9.1(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@6.0.3))(typescript@6.0.3) + '@prisma/client': 7.10.0(prisma@7.10.0(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@6.0.3))(typescript@6.0.3) - '@nestjs/testing@11.1.28(@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.28)(@nestjs/platform-express@11.1.28)': + '@nestjs/testing@12.0.1(@nestjs/common@12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@12.0.1)(@nestjs/platform-express@12.0.1)': dependencies: - '@nestjs/common': 11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.28(@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.28)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 12.0.1(@nestjs/common@12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@12.0.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) tslib: 2.8.1 optionalDependencies: - '@nestjs/platform-express': 11.1.28(@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.28) + '@nestjs/platform-express': 12.0.1(@nestjs/common@12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@12.0.1) - '@next/env@16.2.12': {} + '@next/env@16.3.3': {} - '@next/swc-darwin-arm64@16.2.12': + '@next/swc-darwin-arm64@16.3.3': optional: true - '@next/swc-darwin-x64@16.2.12': + '@next/swc-darwin-x64@16.3.3': optional: true - '@next/swc-linux-arm64-gnu@16.2.12': + '@next/swc-linux-arm64-gnu@16.3.3': optional: true - '@next/swc-linux-arm64-musl@16.2.12': + '@next/swc-linux-arm64-musl@16.3.3': optional: true - '@next/swc-linux-x64-gnu@16.2.12': + '@next/swc-linux-x64-gnu@16.3.3': optional: true - '@next/swc-linux-x64-musl@16.2.12': + '@next/swc-linux-x64-musl@16.3.3': optional: true - '@next/swc-win32-arm64-msvc@16.2.12': + '@next/swc-win32-arm64-msvc@16.3.3': optional: true - '@next/swc-win32-x64-msvc@16.2.12': + '@next/swc-win32-x64-msvc@16.3.3': optional: true '@noble/hashes@1.8.0': {} @@ -6960,7 +6588,7 @@ snapshots: '@nodelib/fs.walk@1.2.8': dependencies: '@nodelib/fs.scandir': 2.1.5 - fastq: 1.20.1 + fastq: 1.20.3 '@opentelemetry/api@1.4.1': {} @@ -6968,30 +6596,86 @@ snapshots: dependencies: '@noble/hashes': 1.8.0 + '@parcel/watcher-android-arm64@2.6.0': + optional: true + + '@parcel/watcher-darwin-arm64@2.6.0': + optional: true + + '@parcel/watcher-darwin-x64@2.6.0': + optional: true + + '@parcel/watcher-freebsd-x64@2.6.0': + optional: true + + '@parcel/watcher-linux-arm-glibc@2.6.0': + optional: true + + '@parcel/watcher-linux-arm-musl@2.6.0': + optional: true + + '@parcel/watcher-linux-arm64-glibc@2.6.0': + optional: true + + '@parcel/watcher-linux-arm64-musl@2.6.0': + optional: true + + '@parcel/watcher-linux-x64-glibc@2.6.0': + optional: true + + '@parcel/watcher-linux-x64-musl@2.6.0': + optional: true + + '@parcel/watcher-win32-arm64@2.6.0': + optional: true + + '@parcel/watcher-win32-x64@2.6.0': + optional: true + + '@parcel/watcher@2.6.0': + dependencies: + detect-libc: 2.1.2 + is-glob: 4.0.3 + node-addon-api: 7.1.1 + picomatch: 4.0.7 + optionalDependencies: + '@parcel/watcher-android-arm64': 2.6.0 + '@parcel/watcher-darwin-arm64': 2.6.0 + '@parcel/watcher-darwin-x64': 2.6.0 + '@parcel/watcher-freebsd-x64': 2.6.0 + '@parcel/watcher-linux-arm-glibc': 2.6.0 + '@parcel/watcher-linux-arm-musl': 2.6.0 + '@parcel/watcher-linux-arm64-glibc': 2.6.0 + '@parcel/watcher-linux-arm64-musl': 2.6.0 + '@parcel/watcher-linux-x64-glibc': 2.6.0 + '@parcel/watcher-linux-x64-musl': 2.6.0 + '@parcel/watcher-win32-arm64': 2.6.0 + '@parcel/watcher-win32-x64': 2.6.0 + '@pkgjs/parseargs@0.11.0': optional: true '@pkgr/core@0.3.6': {} - '@prisma/adapter-pg@7.9.1': + '@prisma/adapter-pg@7.10.0': dependencies: - '@prisma/driver-adapter-utils': 7.9.1 - '@types/pg': 8.20.0 - pg: 8.22.0 + '@prisma/driver-adapter-utils': 7.10.0 + '@types/pg': 8.23.1 + pg: 8.23.0 postgres-array: 3.0.4 transitivePeerDependencies: - pg-native - '@prisma/client-runtime-utils@7.9.1': {} + '@prisma/client-runtime-utils@7.10.0': {} - '@prisma/client@7.9.1(prisma@7.9.1(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@6.0.3))(typescript@6.0.3)': + '@prisma/client@7.10.0(prisma@7.10.0(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@6.0.3))(typescript@6.0.3)': dependencies: - '@prisma/client-runtime-utils': 7.9.1 + '@prisma/client-runtime-utils': 7.10.0 optionalDependencies: - prisma: 7.9.1(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@6.0.3) + prisma: 7.10.0(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@6.0.3) typescript: 6.0.3 - '@prisma/config@7.9.1': + '@prisma/config@7.10.0': dependencies: c12: 3.3.4 deepmerge-ts: 7.1.5 @@ -7008,9 +6692,9 @@ snapshots: transitivePeerDependencies: - supports-color - '@prisma/debug@7.2.0': {} + '@prisma/debug@7.10.0': {} - '@prisma/debug@7.9.1': {} + '@prisma/debug@7.2.0': {} '@prisma/dev@0.24.17(typescript@6.0.3)': dependencies: @@ -7020,7 +6704,7 @@ snapshots: '@prisma/get-platform': 7.2.0 '@prisma/query-plan-executor': 7.2.0 '@prisma/streams-local': 0.1.11 - find-my-way: 9.7.0 + find-my-way: 9.9.0 foreground-child: 3.3.1 get-port-please: 3.2.0 pathe: 2.0.3 @@ -7032,20 +6716,20 @@ snapshots: transitivePeerDependencies: - typescript - '@prisma/driver-adapter-utils@7.9.1': + '@prisma/driver-adapter-utils@7.10.0': dependencies: - '@prisma/debug': 7.9.1 + '@prisma/debug': 7.10.0 - '@prisma/engines-version@7.9.0-1.e922089b7d7502aff4249d5da3420f6fa55fc6ad': {} + '@prisma/engines-version@7.10.0-4.0edf323efd1d98336f3f0a68684b56f689b900d3': {} '@prisma/engines@5.0.0': {} - '@prisma/engines@7.9.1': + '@prisma/engines@7.10.0': dependencies: - '@prisma/debug': 7.9.1 - '@prisma/engines-version': 7.9.0-1.e922089b7d7502aff4249d5da3420f6fa55fc6ad - '@prisma/fetch-engine': 7.9.1 - '@prisma/get-platform': 7.9.1 + '@prisma/debug': 7.10.0 + '@prisma/engines-version': 7.10.0-4.0edf323efd1d98336f3f0a68684b56f689b900d3 + '@prisma/fetch-engine': 7.10.0 + '@prisma/get-platform': 7.10.0 '@prisma/fetch-engine@5.0.0': dependencies: @@ -7070,17 +6754,17 @@ snapshots: - encoding - supports-color - '@prisma/fetch-engine@7.9.1': + '@prisma/fetch-engine@7.10.0': dependencies: - '@prisma/debug': 7.9.1 - '@prisma/engines-version': 7.9.0-1.e922089b7d7502aff4249d5da3420f6fa55fc6ad - '@prisma/get-platform': 7.9.1 + '@prisma/debug': 7.10.0 + '@prisma/engines-version': 7.10.0-4.0edf323efd1d98336f3f0a68684b56f689b900d3 + '@prisma/get-platform': 7.10.0 '@prisma/generator-helper@5.0.0': dependencies: '@prisma/debug': 5.0.0 '@types/cross-spawn': 6.0.2 - cross-spawn: 7.0.3 + cross-spawn: 7.0.6 kleur: 4.1.5 transitivePeerDependencies: - supports-color @@ -7100,13 +6784,13 @@ snapshots: transitivePeerDependencies: - supports-color - '@prisma/get-platform@7.2.0': + '@prisma/get-platform@7.10.0': dependencies: - '@prisma/debug': 7.2.0 + '@prisma/debug': 7.10.0 - '@prisma/get-platform@7.9.1': + '@prisma/get-platform@7.2.0': dependencies: - '@prisma/debug': 7.9.1 + '@prisma/debug': 7.2.0 '@prisma/internals@5.0.0': dependencies: @@ -7151,7 +6835,7 @@ snapshots: temp-write: 4.0.0 tempy: 1.0.1 terminal-link: 2.1.1 - tmp: 0.2.1 + tmp: 0.2.7 ts-pattern: 4.3.0 transitivePeerDependencies: - encoding @@ -7168,10 +6852,10 @@ snapshots: env-paths: 3.0.0 proper-lockfile: 4.1.2 - '@prisma/studio-core@0.33.0(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + '@prisma/studio-core@0.33.0(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: - '@radix-ui/react-toggle': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@types/react': 19.2.17 + '@radix-ui/react-toggle': 1.1.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@types/react': 19.2.18 '@visx/curve': 4.0.1-alpha.0 '@visx/event': 4.0.1-alpha.0 '@visx/grid': 4.0.1-alpha.0(react@19.2.8) @@ -7189,65 +6873,65 @@ snapshots: '@radix-ui/primitive@1.1.3': {} - '@radix-ui/react-compose-refs@1.1.2(@types/react@19.2.17)(react@19.2.8)': + '@radix-ui/react-compose-refs@1.1.2(@types/react@19.2.18)(react@19.2.8)': dependencies: react: 19.2.8 optionalDependencies: - '@types/react': 19.2.17 + '@types/react': 19.2.18 - '@radix-ui/react-primitive@2.1.3(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + '@radix-ui/react-primitive@2.1.3(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: - '@radix-ui/react-slot': 1.2.3(@types/react@19.2.17)(react@19.2.8) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.18)(react@19.2.8) react: 19.2.8 react-dom: 19.2.8(react@19.2.8) optionalDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3(@types/react@19.2.17) + '@types/react': 19.2.18 + '@types/react-dom': 19.2.5(@types/react@19.2.18) - '@radix-ui/react-slot@1.2.3(@types/react@19.2.17)(react@19.2.8)': + '@radix-ui/react-slot@1.2.3(@types/react@19.2.18)(react@19.2.8)': dependencies: - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.17)(react@19.2.8) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.18)(react@19.2.8) react: 19.2.8 optionalDependencies: - '@types/react': 19.2.17 + '@types/react': 19.2.18 - '@radix-ui/react-toggle@1.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + '@radix-ui/react-toggle@1.1.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.17)(react@19.2.8) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.18)(react@19.2.8) react: 19.2.8 react-dom: 19.2.8(react@19.2.8) optionalDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3(@types/react@19.2.17) + '@types/react': 19.2.18 + '@types/react-dom': 19.2.5(@types/react@19.2.18) - '@radix-ui/react-use-controllable-state@1.2.2(@types/react@19.2.17)(react@19.2.8)': + '@radix-ui/react-use-controllable-state@1.2.2(@types/react@19.2.18)(react@19.2.8)': dependencies: - '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.17)(react@19.2.8) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.17)(react@19.2.8) + '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.18)(react@19.2.8) react: 19.2.8 optionalDependencies: - '@types/react': 19.2.17 + '@types/react': 19.2.18 - '@radix-ui/react-use-effect-event@0.0.2(@types/react@19.2.17)(react@19.2.8)': + '@radix-ui/react-use-effect-event@0.0.2(@types/react@19.2.18)(react@19.2.8)': dependencies: - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.17)(react@19.2.8) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.18)(react@19.2.8) react: 19.2.8 optionalDependencies: - '@types/react': 19.2.17 + '@types/react': 19.2.18 - '@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.2.17)(react@19.2.8)': + '@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.2.18)(react@19.2.8)': dependencies: react: 19.2.8 optionalDependencies: - '@types/react': 19.2.17 - - '@rtsao/scc@1.1.0': {} + '@types/react': 19.2.18 '@scarf/scarf@1.4.0': {} - '@sinclair/typebox@0.34.49': {} + '@sinclair/typebox@0.34.52': {} + + '@sindresorhus/is@4.6.0': {} '@sinonjs/commons@3.0.1': dependencies: @@ -7257,49 +6941,49 @@ snapshots: dependencies: '@sinonjs/commons': 3.0.1 - '@smithy/core@3.31.1': + '@smithy/core@3.33.3': dependencies: - '@smithy/types': 4.16.1 + '@smithy/types': 4.17.2 tslib: 2.8.1 - '@smithy/credential-provider-imds@4.4.16': + '@smithy/credential-provider-imds@4.5.2': dependencies: - '@smithy/core': 3.31.1 - '@smithy/types': 4.16.1 + '@smithy/core': 3.33.3 + '@smithy/types': 4.17.2 tslib: 2.8.1 - '@smithy/fetch-http-handler@5.6.13': + '@smithy/fetch-http-handler@5.7.2': dependencies: - '@smithy/core': 3.31.1 - '@smithy/types': 4.16.1 + '@smithy/core': 3.33.3 + '@smithy/types': 4.17.2 tslib: 2.8.1 - '@smithy/node-http-handler@4.9.13': + '@smithy/node-http-handler@4.11.3': dependencies: - '@smithy/core': 3.31.1 - '@smithy/types': 4.16.1 + '@smithy/core': 3.33.3 + '@smithy/types': 4.17.2 tslib: 2.8.1 - '@smithy/signature-v4@5.6.12': + '@smithy/signature-v4@5.7.3': dependencies: - '@smithy/core': 3.31.1 - '@smithy/types': 4.16.1 + '@smithy/core': 3.33.3 + '@smithy/types': 4.17.2 tslib: 2.8.1 - '@smithy/types@4.16.1': + '@smithy/types@4.17.2': dependencies: tslib: 2.8.1 '@standard-schema/spec@1.1.0': {} - '@swc/helpers@0.5.15': + '@swc/helpers@0.5.23': dependencies: tslib: 2.8.1 '@tailwindcss/node@4.3.3': dependencies: '@jridgewell/remapping': 2.3.5 - enhanced-resolve: 5.24.3 + enhanced-resolve: 5.24.5 jiti: 2.7.0 lightningcss: 1.32.0 magic-string: 0.30.21 @@ -7362,7 +7046,7 @@ snapshots: '@alloc/quick-lru': 5.2.0 '@tailwindcss/node': 4.3.3 '@tailwindcss/oxide': 4.3.3 - postcss: 8.5.23 + postcss: 8.5.26 tailwindcss: 4.3.3 '@tokenizer/inflate@0.4.1': @@ -7374,7 +7058,7 @@ snapshots: '@tokenizer/token@0.3.0': {} - '@tsconfig/node10@1.0.12': {} + '@tsconfig/node10@1.0.13': {} '@tsconfig/node12@1.0.11': {} @@ -7382,22 +7066,22 @@ snapshots: '@tsconfig/node16@1.0.4': {} - '@turbo/darwin-64@2.10.7': + '@turbo/darwin-64@2.10.12': optional: true - '@turbo/darwin-arm64@2.10.7': + '@turbo/darwin-arm64@2.10.12': optional: true - '@turbo/linux-64@2.10.7': + '@turbo/linux-64@2.10.12': optional: true - '@turbo/linux-arm64@2.10.7': + '@turbo/linux-arm64@2.10.12': optional: true - '@turbo/windows-64@2.10.7': + '@turbo/windows-64@2.10.12': optional: true - '@turbo/windows-arm64@2.10.7': + '@turbo/windows-arm64@2.10.12': optional: true '@tybys/wasm-util@0.10.3': @@ -7407,42 +7091,42 @@ snapshots: '@types/archiver@8.0.0': dependencies: - '@types/node': 26.1.2 + '@types/node': 26.4.0 '@types/readdir-glob': 1.1.5 '@types/babel__core@7.20.5': dependencies: - '@babel/parser': 7.29.7 - '@babel/types': 7.29.7 + '@babel/parser': 7.29.8 + '@babel/types': 7.29.8 '@types/babel__generator': 7.27.0 '@types/babel__template': 7.4.4 '@types/babel__traverse': 7.28.0 '@types/babel__generator@7.27.0': dependencies: - '@babel/types': 7.29.7 + '@babel/types': 7.29.8 '@types/babel__template@7.4.4': dependencies: - '@babel/parser': 7.29.7 - '@babel/types': 7.29.7 + '@babel/parser': 7.29.8 + '@babel/types': 7.29.8 '@types/babel__traverse@7.28.0': dependencies: - '@babel/types': 7.29.7 + '@babel/types': 7.29.8 '@types/body-parser@1.19.6': dependencies: '@types/connect': 3.4.38 - '@types/node': 26.1.2 + '@types/node': 26.4.0 '@types/busboy@1.5.4': dependencies: - '@types/node': 26.1.2 + '@types/node': 26.4.0 '@types/connect@3.4.38': dependencies: - '@types/node': 26.1.2 + '@types/node': 26.4.0 '@types/cookie-parser@1.4.10(@types/express@5.0.6)': dependencies: @@ -7452,7 +7136,7 @@ snapshots: '@types/cross-spawn@6.0.2': dependencies: - '@types/node': 26.1.2 + '@types/node': 26.4.0 '@types/d3-array@3.0.3': {} @@ -7492,19 +7176,21 @@ snapshots: dependencies: '@types/eslint': 9.6.1 '@types/estree': 1.0.9 + optional: true '@types/eslint@9.6.1': dependencies: '@types/estree': 1.0.9 '@types/json-schema': 7.0.15 + optional: true '@types/esrecurse@4.3.1': {} '@types/estree@1.0.9': {} - '@types/express-serve-static-core@5.1.1': + '@types/express-serve-static-core@5.1.3': dependencies: - '@types/node': 26.1.2 + '@types/node': 26.4.0 '@types/qs': 6.15.1 '@types/range-parser': 1.2.7 '@types/send': 1.2.1 @@ -7512,7 +7198,7 @@ snapshots: '@types/express@5.0.6': dependencies: '@types/body-parser': 1.19.6 - '@types/express-serve-static-core': 5.1.1 + '@types/express-serve-static-core': 5.1.3 '@types/serve-static': 2.2.0 '@types/geojson@7946.0.16': {} @@ -7531,76 +7217,74 @@ snapshots: '@types/jest@30.0.0': dependencies: - expect: 30.4.1 - pretty-format: 30.4.1 + expect: 30.5.0 + pretty-format: 30.5.0 '@types/json-schema@7.0.15': {} - '@types/json5@0.0.29': {} - '@types/jsonwebtoken@9.0.10': dependencies: '@types/ms': 2.1.0 - '@types/node': 26.1.2 + '@types/node': 26.4.0 - '@types/lodash@4.17.24': {} + '@types/lodash@4.17.25': {} '@types/methods@1.1.4': {} '@types/ms@2.1.0': {} - '@types/node@26.1.2': + '@types/node@26.4.0': dependencies: undici-types: 8.3.0 '@types/normalize-package-data@2.4.4': {} - '@types/pg@8.20.0': + '@types/pg@8.23.1': dependencies: - '@types/node': 26.1.2 - pg-protocol: 1.15.0 + '@types/node': 26.4.0 + pg-protocol: 1.16.0 pg-types: 2.2.0 '@types/qs@6.15.1': {} '@types/range-parser@1.2.7': {} - '@types/react-dom@19.2.3(@types/react@19.2.17)': + '@types/react-dom@19.2.5(@types/react@19.2.18)': dependencies: - '@types/react': 19.2.17 + '@types/react': 19.2.18 - '@types/react@19.2.17': + '@types/react@19.2.18': dependencies: csstype: 3.2.3 '@types/readdir-glob@1.1.5': dependencies: - '@types/node': 26.1.2 + '@types/node': 26.4.0 '@types/retry@0.12.0': {} '@types/send@1.2.1': dependencies: - '@types/node': 26.1.2 + '@types/node': 26.4.0 '@types/serve-static@2.2.0': dependencies: '@types/http-errors': 2.0.5 - '@types/node': 26.1.2 + '@types/node': 26.4.0 '@types/stack-utils@2.0.3': {} - '@types/superagent@8.1.10': + '@types/superagent@8.1.11': dependencies: '@types/cookiejar': 2.1.5 '@types/methods': 1.1.4 - '@types/node': 26.1.2 + '@types/node': 26.4.0 form-data: 4.0.6 '@types/supertest@7.2.1': dependencies: '@types/methods': 1.1.4 - '@types/superagent': 8.1.10 + '@types/superagent': 8.1.11 '@types/validator@13.15.10': {} @@ -7610,74 +7294,74 @@ snapshots: dependencies: '@types/yargs-parser': 21.0.3 - '@typescript-eslint/eslint-plugin@8.65.0(@typescript-eslint/parser@8.65.0(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3)': + '@typescript-eslint/eslint-plugin@8.68.0(@typescript-eslint/parser@8.68.0(eslint@10.9.1(jiti@2.7.0))(typescript@6.0.3))(eslint@10.9.1(jiti@2.7.0))(typescript@6.0.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.65.0(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3) - '@typescript-eslint/scope-manager': 8.65.0 - '@typescript-eslint/type-utils': 8.65.0(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3) - '@typescript-eslint/utils': 8.65.0(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3) - '@typescript-eslint/visitor-keys': 8.65.0 - eslint: 10.8.0(jiti@2.7.0) - ignore: 7.0.5 + '@typescript-eslint/parser': 8.68.0(eslint@10.9.1(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/scope-manager': 8.68.0 + '@typescript-eslint/type-utils': 8.68.0(eslint@10.9.1(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/utils': 8.68.0(eslint@10.9.1(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/visitor-keys': 8.68.0 + eslint: 10.9.1(jiti@2.7.0) + ignore: 7.0.7 natural-compare: 1.4.0 ts-api-utils: 2.5.0(typescript@6.0.3) typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.65.0(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3)': + '@typescript-eslint/parser@8.68.0(eslint@10.9.1(jiti@2.7.0))(typescript@6.0.3)': dependencies: - '@typescript-eslint/scope-manager': 8.65.0 - '@typescript-eslint/types': 8.65.0 - '@typescript-eslint/typescript-estree': 8.65.0(typescript@6.0.3) - '@typescript-eslint/visitor-keys': 8.65.0 + '@typescript-eslint/scope-manager': 8.68.0 + '@typescript-eslint/types': 8.68.0 + '@typescript-eslint/typescript-estree': 8.68.0(typescript@6.0.3) + '@typescript-eslint/visitor-keys': 8.68.0 debug: 4.4.3 - eslint: 10.8.0(jiti@2.7.0) + eslint: 10.9.1(jiti@2.7.0) typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.65.0(typescript@6.0.3)': + '@typescript-eslint/project-service@8.68.0(typescript@6.0.3)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.65.0(typescript@6.0.3) - '@typescript-eslint/types': 8.65.0 + '@typescript-eslint/tsconfig-utils': 8.68.0(typescript@6.0.3) + '@typescript-eslint/types': 8.68.0 debug: 4.4.3 typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@8.65.0': + '@typescript-eslint/scope-manager@8.68.0': dependencies: - '@typescript-eslint/types': 8.65.0 - '@typescript-eslint/visitor-keys': 8.65.0 + '@typescript-eslint/types': 8.68.0 + '@typescript-eslint/visitor-keys': 8.68.0 - '@typescript-eslint/tsconfig-utils@8.65.0(typescript@6.0.3)': + '@typescript-eslint/tsconfig-utils@8.68.0(typescript@6.0.3)': dependencies: typescript: 6.0.3 - '@typescript-eslint/type-utils@8.65.0(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3)': + '@typescript-eslint/type-utils@8.68.0(eslint@10.9.1(jiti@2.7.0))(typescript@6.0.3)': dependencies: - '@typescript-eslint/types': 8.65.0 - '@typescript-eslint/typescript-estree': 8.65.0(typescript@6.0.3) - '@typescript-eslint/utils': 8.65.0(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/types': 8.68.0 + '@typescript-eslint/typescript-estree': 8.68.0(typescript@6.0.3) + '@typescript-eslint/utils': 8.68.0(eslint@10.9.1(jiti@2.7.0))(typescript@6.0.3) debug: 4.4.3 - eslint: 10.8.0(jiti@2.7.0) + eslint: 10.9.1(jiti@2.7.0) ts-api-utils: 2.5.0(typescript@6.0.3) typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/types@8.65.0': {} + '@typescript-eslint/types@8.68.0': {} - '@typescript-eslint/typescript-estree@8.65.0(typescript@6.0.3)': + '@typescript-eslint/typescript-estree@8.68.0(typescript@6.0.3)': dependencies: - '@typescript-eslint/project-service': 8.65.0(typescript@6.0.3) - '@typescript-eslint/tsconfig-utils': 8.65.0(typescript@6.0.3) - '@typescript-eslint/types': 8.65.0 - '@typescript-eslint/visitor-keys': 8.65.0 + '@typescript-eslint/project-service': 8.68.0(typescript@6.0.3) + '@typescript-eslint/tsconfig-utils': 8.68.0(typescript@6.0.3) + '@typescript-eslint/types': 8.68.0 + '@typescript-eslint/visitor-keys': 8.68.0 debug: 4.4.3 - minimatch: 10.2.5 + minimatch: 10.2.6 semver: 7.8.5 tinyglobby: 0.2.17 ts-api-utils: 2.5.0(typescript@6.0.3) @@ -7685,23 +7369,23 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.65.0(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3)': + '@typescript-eslint/utils@8.68.0(eslint@10.9.1(jiti@2.7.0))(typescript@6.0.3)': dependencies: - '@eslint-community/eslint-utils': 4.10.1(eslint@10.8.0(jiti@2.7.0)) - '@typescript-eslint/scope-manager': 8.65.0 - '@typescript-eslint/types': 8.65.0 - '@typescript-eslint/typescript-estree': 8.65.0(typescript@6.0.3) - eslint: 10.8.0(jiti@2.7.0) + '@eslint-community/eslint-utils': 4.10.1(eslint@10.9.1(jiti@2.7.0)) + '@typescript-eslint/scope-manager': 8.68.0 + '@typescript-eslint/types': 8.68.0 + '@typescript-eslint/typescript-estree': 8.68.0(typescript@6.0.3) + eslint: 10.9.1(jiti@2.7.0) typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/visitor-keys@8.65.0': + '@typescript-eslint/visitor-keys@8.68.0': dependencies: - '@typescript-eslint/types': 8.65.0 + '@typescript-eslint/types': 8.68.0 eslint-visitor-keys: 5.0.1 - '@ungap/structured-clone@1.3.2': {} + '@ungap/structured-clone@1.4.0': {} '@unrs/resolver-binding-android-arm-eabi@1.12.2': optional: true @@ -7761,7 +7445,7 @@ snapshots: dependencies: '@emnapi/core': 1.10.0 '@emnapi/runtime': 1.10.0 - '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + '@napi-rs/wasm-runtime': 1.2.3(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) optional: true '@unrs/resolver-binding-win32-arm64-msvc@1.12.2': @@ -7779,12 +7463,12 @@ snapshots: '@visx/event@4.0.1-alpha.0': dependencies: - '@types/react': 19.2.17 + '@types/react': 19.2.18 '@visx/point': 4.0.1-alpha.0 '@visx/grid@4.0.1-alpha.0(react@19.2.8)': dependencies: - '@types/react': 19.2.17 + '@types/react': 19.2.18 '@visx/curve': 4.0.1-alpha.0 '@visx/group': 4.0.1-alpha.0(react@19.2.8) '@visx/point': 4.0.1-alpha.0 @@ -7795,7 +7479,7 @@ snapshots: '@visx/group@4.0.1-alpha.0(react@19.2.8)': dependencies: - '@types/react': 19.2.17 + '@types/react': 19.2.18 classnames: 2.5.1 react: 19.2.8 @@ -7803,8 +7487,8 @@ snapshots: '@visx/responsive@4.0.1-alpha.0(react@19.2.8)': dependencies: - '@types/lodash': 4.17.24 - '@types/react': 19.2.17 + '@types/lodash': 4.17.25 + '@types/react': 19.2.18 lodash: 4.18.1 react: 19.2.8 @@ -7814,8 +7498,8 @@ snapshots: '@visx/shape@4.0.1-alpha.0(react@19.2.8)': dependencies: - '@types/lodash': 4.17.24 - '@types/react': 19.2.17 + '@types/lodash': 4.17.25 + '@types/react': 19.2.18 '@visx/curve': 4.0.1-alpha.0 '@visx/group': 4.0.1-alpha.0(react@19.2.8) '@visx/scale': 4.0.1-alpha.0 @@ -7854,20 +7538,26 @@ snapshots: dependencies: '@webassemblyjs/helper-numbers': 1.13.2 '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + optional: true - '@webassemblyjs/floating-point-hex-parser@1.13.2': {} + '@webassemblyjs/floating-point-hex-parser@1.13.2': + optional: true - '@webassemblyjs/helper-api-error@1.13.2': {} + '@webassemblyjs/helper-api-error@1.13.2': + optional: true - '@webassemblyjs/helper-buffer@1.14.1': {} + '@webassemblyjs/helper-buffer@1.14.1': + optional: true '@webassemblyjs/helper-numbers@1.13.2': dependencies: '@webassemblyjs/floating-point-hex-parser': 1.13.2 '@webassemblyjs/helper-api-error': 1.13.2 '@xtuc/long': 4.2.2 + optional: true - '@webassemblyjs/helper-wasm-bytecode@1.13.2': {} + '@webassemblyjs/helper-wasm-bytecode@1.13.2': + optional: true '@webassemblyjs/helper-wasm-section@1.14.1': dependencies: @@ -7875,16 +7565,20 @@ snapshots: '@webassemblyjs/helper-buffer': 1.14.1 '@webassemblyjs/helper-wasm-bytecode': 1.13.2 '@webassemblyjs/wasm-gen': 1.14.1 + optional: true '@webassemblyjs/ieee754@1.13.2': dependencies: '@xtuc/ieee754': 1.2.0 + optional: true '@webassemblyjs/leb128@1.13.2': dependencies: '@xtuc/long': 4.2.2 + optional: true - '@webassemblyjs/utf8@1.13.2': {} + '@webassemblyjs/utf8@1.13.2': + optional: true '@webassemblyjs/wasm-edit@1.14.1': dependencies: @@ -7896,6 +7590,7 @@ snapshots: '@webassemblyjs/wasm-opt': 1.14.1 '@webassemblyjs/wasm-parser': 1.14.1 '@webassemblyjs/wast-printer': 1.14.1 + optional: true '@webassemblyjs/wasm-gen@1.14.1': dependencies: @@ -7904,6 +7599,7 @@ snapshots: '@webassemblyjs/ieee754': 1.13.2 '@webassemblyjs/leb128': 1.13.2 '@webassemblyjs/utf8': 1.13.2 + optional: true '@webassemblyjs/wasm-opt@1.14.1': dependencies: @@ -7911,6 +7607,7 @@ snapshots: '@webassemblyjs/helper-buffer': 1.14.1 '@webassemblyjs/wasm-gen': 1.14.1 '@webassemblyjs/wasm-parser': 1.14.1 + optional: true '@webassemblyjs/wasm-parser@1.14.1': dependencies: @@ -7920,15 +7617,19 @@ snapshots: '@webassemblyjs/ieee754': 1.13.2 '@webassemblyjs/leb128': 1.13.2 '@webassemblyjs/utf8': 1.13.2 + optional: true '@webassemblyjs/wast-printer@1.14.1': dependencies: '@webassemblyjs/ast': 1.14.1 '@xtuc/long': 4.2.2 + optional: true - '@xtuc/ieee754@1.2.0': {} + '@xtuc/ieee754@1.2.0': + optional: true - '@xtuc/long@4.2.2': {} + '@xtuc/long@4.2.2': + optional: true abort-controller@3.0.0: dependencies: @@ -7937,21 +7638,22 @@ snapshots: accepts@2.0.0: dependencies: mime-types: 3.0.2 - negotiator: 1.0.0 + negotiator: 1.1.0 - acorn-import-phases@1.0.4(acorn@8.17.0): + acorn-import-phases@1.0.4(acorn@8.18.0): dependencies: - acorn: 8.17.0 + acorn: 8.18.0 + optional: true - acorn-jsx@5.3.2(acorn@8.17.0): + acorn-jsx@5.3.2(acorn@8.18.0): dependencies: - acorn: 8.17.0 + acorn: 8.18.0 acorn-walk@8.3.5: dependencies: - acorn: 8.17.0 + acorn: 8.18.0 - acorn@8.17.0: {} + acorn@8.18.0: {} agent-base@6.0.2: dependencies: @@ -7969,19 +7671,22 @@ snapshots: ajv-formats@2.1.1(ajv@8.20.0): optionalDependencies: ajv: 8.20.0 + optional: true - ajv-formats@3.0.1(ajv@8.18.0): + ajv-formats@3.0.1(ajv@8.20.0): optionalDependencies: - ajv: 8.18.0 + ajv: 8.20.0 ajv-keywords@3.5.2(ajv@6.15.0): dependencies: ajv: 6.15.0 + optional: true ajv-keywords@5.1.0(ajv@8.20.0): dependencies: ajv: 8.20.0 fast-deep-equal: 3.1.3 + optional: true ajv@6.15.0: dependencies: @@ -7990,17 +7695,10 @@ snapshots: json-schema-traverse: 0.4.1 uri-js: 4.4.1 - ajv@8.18.0: - dependencies: - fast-deep-equal: 3.1.3 - fast-uri: 3.1.4 - json-schema-traverse: 1.0.0 - require-from-string: 2.0.2 - ajv@8.20.0: dependencies: fast-deep-equal: 3.1.3 - fast-uri: 3.1.4 + fast-uri: 3.1.5 json-schema-traverse: 1.0.0 require-from-string: 2.0.2 @@ -8008,15 +7706,13 @@ snapshots: dependencies: string-width: 4.2.3 - ansi-colors@4.1.3: {} - ansi-escapes@4.3.2: dependencies: type-fest: 0.21.3 ansi-regex@5.0.1: {} - ansi-regex@6.2.2: {} + ansi-regex@6.3.0: {} ansi-styles@4.3.0: dependencies: @@ -8026,7 +7722,7 @@ snapshots: ansi-styles@6.2.3: {} - ansis@4.2.0: {} + ansis@4.3.1: {} anymatch@3.1.3: dependencies: @@ -8080,7 +7776,7 @@ snapshots: normalize-path: 3.0.0 readable-stream: 4.7.0 readdir-glob: 3.0.0 - tar-stream: 3.2.0 + tar-stream: 3.2.1 zip-stream: 7.0.5 transitivePeerDependencies: - bare-abort-controller @@ -8097,77 +7793,21 @@ snapshots: argparse@2.0.1: {} - array-buffer-byte-length@1.0.2: - dependencies: - call-bound: 1.0.4 - is-array-buffer: 3.0.5 - - array-includes@3.1.9: - dependencies: - call-bind: 1.0.9 - call-bound: 1.0.4 - define-properties: 1.2.1 - es-abstract: 1.24.2 - es-object-atoms: 1.1.2 - get-intrinsic: 1.3.0 - is-string: 1.1.1 - math-intrinsics: 1.1.0 - array-timsort@1.0.3: {} array-union@2.1.0: {} - array.prototype.findlastindex@1.2.6: - dependencies: - call-bind: 1.0.9 - call-bound: 1.0.4 - define-properties: 1.2.1 - es-abstract: 1.24.2 - es-errors: 1.3.0 - es-object-atoms: 1.1.2 - es-shim-unscopables: 1.1.0 - - array.prototype.flat@1.3.3: - dependencies: - call-bind: 1.0.9 - define-properties: 1.2.1 - es-abstract: 1.24.2 - es-shim-unscopables: 1.1.0 - - array.prototype.flatmap@1.3.3: - dependencies: - call-bind: 1.0.9 - define-properties: 1.2.1 - es-abstract: 1.24.2 - es-shim-unscopables: 1.1.0 - - arraybuffer.prototype.slice@1.0.4: - dependencies: - array-buffer-byte-length: 1.0.2 - call-bind: 1.0.9 - define-properties: 1.2.1 - es-abstract: 1.24.2 - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - is-array-buffer: 3.0.5 - asap@2.0.6: {} astral-regex@2.0.0: {} - async-function@1.0.0: {} - async@3.2.6: {} asynckit@0.4.0: {} - available-typed-arrays@1.0.7: - dependencies: - possible-typed-array-names: 1.1.0 - aws-ssl-profiles@1.1.2: {} - axios@1.19.0: + axios@1.20.0: dependencies: follow-redirects: 1.16.0 form-data: 4.0.6 @@ -8179,30 +7819,30 @@ snapshots: b4a@1.8.1: {} - babel-jest@30.4.1(@babel/core@7.29.7): + babel-jest@30.5.0(@babel/core@7.29.7): dependencies: '@babel/core': 7.29.7 - '@jest/transform': 30.4.1 + '@jest/transform': 30.5.0 '@types/babel__core': 7.20.5 - babel-plugin-istanbul: 7.0.1 - babel-preset-jest: 30.4.0(@babel/core@7.29.7) + babel-plugin-istanbul: 8.0.0 + babel-preset-jest: 30.5.0(@babel/core@7.29.7) chalk: 4.1.2 graceful-fs: 4.2.11 slash: 3.0.0 transitivePeerDependencies: - supports-color - babel-plugin-istanbul@7.0.1: + babel-plugin-istanbul@8.0.0: dependencies: '@babel/helper-plugin-utils': 7.29.7 '@istanbuljs/load-nyc-config': 1.1.0 '@istanbuljs/schema': 0.1.6 istanbul-lib-instrument: 6.0.3 - test-exclude: 6.0.0 + test-exclude: 7.0.2 transitivePeerDependencies: - supports-color - babel-plugin-jest-hoist@30.4.0: + babel-plugin-jest-hoist@30.5.0: dependencies: '@types/babel__core': 7.20.5 @@ -8225,50 +7865,46 @@ snapshots: '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.29.7) '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.29.7) - babel-preset-jest@30.4.0(@babel/core@7.29.7): + babel-preset-jest@30.5.0(@babel/core@7.29.7): dependencies: '@babel/core': 7.29.7 - babel-plugin-jest-hoist: 30.4.0 + babel-plugin-jest-hoist: 30.5.0 babel-preset-current-node-syntax: 1.2.0(@babel/core@7.29.7) balanced-match@4.0.4: {} - bare-events@2.9.1: {} + bare-events@2.9.2: {} - bare-fs@4.7.3: + bare-fs@4.8.1: dependencies: - bare-events: 2.9.1 - bare-path: 3.0.1 - bare-stream: 2.13.3(bare-events@2.9.1) - bare-url: 2.4.5 + bare-events: 2.9.2 + bare-path: 3.1.1 + bare-stream: 2.13.4(bare-events@2.9.2) + bare-url: 2.5.2 fast-fifo: 1.3.2 transitivePeerDependencies: - bare-abort-controller - react-native-b4a - bare-os@3.9.3: {} - - bare-path@3.0.1: - dependencies: - bare-os: 3.9.3 + bare-path@3.1.1: {} - bare-stream@2.13.3(bare-events@2.9.1): + bare-stream@2.13.4(bare-events@2.9.2): dependencies: b4a: 1.8.1 - streamx: 2.28.0 + streamx: 2.28.1 teex: 1.0.1 optionalDependencies: - bare-events: 2.9.1 + bare-events: 2.9.2 transitivePeerDependencies: - react-native-b4a - bare-url@2.4.5: + bare-url@2.5.2: dependencies: - bare-path: 3.0.1 + bare-path: 3.1.1 base64-js@1.5.1: {} - baseline-browser-mapping@2.10.42: {} + baseline-browser-mapping@2.11.20: {} better-result@2.10.0: {} @@ -8283,12 +7919,12 @@ snapshots: body-parser@2.3.0: dependencies: bytes: 3.1.2 - content-type: 2.0.0 + 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.15.3 + qs: 6.16.0 raw-body: 3.0.2 type-is: 2.1.0 transitivePeerDependencies: @@ -8307,7 +7943,7 @@ snapshots: widest-line: 3.1.0 wrap-ansi: 7.0.0 - brace-expansion@5.0.8: + brace-expansion@5.0.9: dependencies: balanced-match: 4.0.4 @@ -8315,13 +7951,13 @@ snapshots: dependencies: fill-range: 7.1.1 - browserslist@4.28.4: + browserslist@4.28.8: dependencies: - baseline-browser-mapping: 2.10.42 - caniuse-lite: 1.0.30001803 - electron-to-chromium: 1.5.387 - node-releases: 2.0.50 - update-browserslist-db: 1.2.3(browserslist@4.28.4) + baseline-browser-mapping: 2.11.20 + caniuse-lite: 1.0.30001810 + electron-to-chromium: 1.5.416 + node-releases: 2.0.54 + update-browserslist-db: 1.3.2(browserslist@4.28.8) bs-logger@0.2.6: dependencies: @@ -8366,10 +8002,10 @@ snapshots: confbox: 0.2.4 defu: 6.1.7 dotenv: 17.4.2 - exsolve: 1.1.0 + exsolve: 1.1.1 giget: 3.3.1 jiti: 2.7.0 - ohash: 2.0.11 + ohash: 2.0.12 pathe: 2.0.3 perfect-debounce: 2.1.0 pkg-types: 2.3.1 @@ -8380,13 +8016,6 @@ snapshots: es-errors: 1.3.0 function-bind: 1.1.2 - call-bind@1.0.9: - dependencies: - call-bind-apply-helpers: 1.0.2 - es-define-property: 1.0.1 - get-intrinsic: 1.3.0 - set-function-length: 1.2.2 - call-bound@1.0.4: dependencies: call-bind-apply-helpers: 1.0.2 @@ -8398,13 +8027,15 @@ snapshots: camelcase@6.3.0: {} - caniuse-lite@1.0.30001803: {} + caniuse-lite@1.0.30001810: {} chalk@4.1.2: dependencies: ansi-styles: 4.3.0 supports-color: 7.2.0 + chalk@5.6.2: {} + char-regex@1.0.2: {} chardet@2.2.0: {} @@ -8426,25 +8057,27 @@ snapshots: chokidar@4.0.3: dependencies: readdirp: 4.1.2 + optional: true chokidar@5.0.0: dependencies: - readdirp: 5.0.0 + readdirp: 5.1.1 - chrome-trace-event@1.0.4: {} + chrome-trace-event@1.0.4: + optional: true ci-info@3.8.0: {} ci-info@4.4.0: {} - cjs-module-lexer@2.2.0: {} + cjs-module-lexer@2.2.1: {} class-transformer@0.5.1: {} class-validator@0.15.1: dependencies: '@types/validator': 13.15.10 - libphonenumber-js: 1.13.8 + libphonenumber-js: 1.13.12 validator: 13.15.35 classnames@2.5.1: {} @@ -8453,11 +8086,11 @@ snapshots: cli-boxes@2.2.1: {} - cli-cursor@3.1.0: + cli-cursor@5.0.0: dependencies: - restore-cursor: 3.1.0 + restore-cursor: 5.1.0 - cli-spinners@2.9.2: {} + cli-spinners@3.4.0: {} cli-table3@0.6.5: dependencies: @@ -8480,8 +8113,6 @@ snapshots: strip-ansi: 6.0.1 wrap-ansi: 7.0.0 - clone@1.0.4: {} - cluster-key-slot@1.1.1: {} co@4.6.0: {} @@ -8498,9 +8129,10 @@ snapshots: dependencies: delayed-stream: 1.0.0 - commander@2.20.3: {} + commander@15.0.0: {} - commander@4.1.1: {} + commander@2.20.3: + optional: true comment-json@5.0.0: dependencies: @@ -8539,7 +8171,7 @@ snapshots: content-type@1.0.5: {} - content-type@2.0.0: {} + content-type@2.1.0: {} convert-source-map@2.0.0: {} @@ -8565,14 +8197,15 @@ snapshots: object-assign: 4.1.1 vary: 1.1.2 - cosmiconfig@8.3.6(typescript@5.9.3): + cosmiconfig@8.3.6(typescript@6.0.3): dependencies: import-fresh: 3.3.1 - js-yaml: 4.3.0 + js-yaml: 4.3.2 parse-json: 5.2.0 path-type: 4.0.0 optionalDependencies: - typescript: 5.9.3 + typescript: 6.0.3 + optional: true crc-32@1.2.2: {} @@ -8588,12 +8221,6 @@ snapshots: create-require@1.1.1: {} - cross-spawn@7.0.3: - dependencies: - path-key: 3.1.1 - shebang-command: 2.0.0 - which: 2.0.2 - cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -8652,28 +8279,6 @@ snapshots: data-uri-to-buffer@4.0.1: {} - data-view-buffer@1.0.2: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - is-data-view: 1.0.2 - - data-view-byte-length@1.0.2: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - is-data-view: 1.0.2 - - data-view-byte-offset@1.0.1: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - is-data-view: 1.0.2 - - debug@3.2.7: - dependencies: - ms: 2.1.3 - debug@4.3.4: dependencies: ms: 2.1.2 @@ -8690,22 +8295,6 @@ snapshots: deepmerge@4.3.1: {} - defaults@1.0.4: - dependencies: - clone: 1.0.4 - - define-data-property@1.1.4: - dependencies: - es-define-property: 1.0.1 - es-errors: 1.3.0 - gopd: 1.2.0 - - define-properties@1.2.1: - dependencies: - define-data-property: 1.1.4 - has-property-descriptors: 1.0.2 - object-keys: 1.1.1 - defu@6.1.7: {} del@6.1.1: @@ -8746,20 +8335,12 @@ snapshots: dependencies: path-type: 4.0.0 - doctrine@2.1.0: + dotenv-expand@13.0.0: dependencies: - esutils: 2.0.3 - - dotenv-expand@12.0.3: - dependencies: - dotenv: 16.6.1 + dotenv: 17.4.2 dotenv@16.0.3: {} - dotenv@16.6.1: {} - - dotenv@17.4.1: {} - dotenv@17.4.2: {} dunder-proto@1.0.1: @@ -8781,7 +8362,7 @@ snapshots: '@standard-schema/spec': 1.1.0 fast-check: 3.23.2 - electron-to-chromium@1.5.387: {} + electron-to-chromium@1.5.416: {} elkjs@0.11.1: {} @@ -8791,6 +8372,8 @@ snapshots: emoji-regex@9.2.2: {} + emojilib@2.4.0: {} + empathic@2.0.0: {} encodeurl@2.0.0: {} @@ -8799,12 +8382,7 @@ snapshots: dependencies: once: 1.4.0 - enhanced-resolve@5.24.1: - dependencies: - graceful-fs: 4.2.11 - tapable: 2.3.3 - - enhanced-resolve@5.24.3: + enhanced-resolve@5.24.5: dependencies: graceful-fs: 4.2.11 tapable: 2.3.3 @@ -8817,75 +8395,11 @@ snapshots: dependencies: is-arrayish: 0.2.1 - es-abstract-get@1.0.0: - dependencies: - es-errors: 1.3.0 - es-object-atoms: 1.1.2 - is-callable: 1.2.7 - object-inspect: 1.13.4 - - es-abstract@1.24.2: - dependencies: - array-buffer-byte-length: 1.0.2 - arraybuffer.prototype.slice: 1.0.4 - available-typed-arrays: 1.0.7 - call-bind: 1.0.9 - call-bound: 1.0.4 - data-view-buffer: 1.0.2 - data-view-byte-length: 1.0.2 - data-view-byte-offset: 1.0.1 - es-define-property: 1.0.1 - es-errors: 1.3.0 - es-object-atoms: 1.1.2 - es-set-tostringtag: 2.1.0 - es-to-primitive: 1.3.4 - function.prototype.name: 1.2.0 - get-intrinsic: 1.3.0 - get-proto: 1.0.1 - get-symbol-description: 1.1.0 - globalthis: 1.0.4 - gopd: 1.2.0 - has-property-descriptors: 1.0.2 - has-proto: 1.2.0 - has-symbols: 1.1.0 - hasown: 2.0.4 - internal-slot: 1.1.0 - is-array-buffer: 3.0.5 - is-callable: 1.2.7 - is-data-view: 1.0.2 - is-negative-zero: 2.0.3 - is-regex: 1.2.1 - is-set: 2.0.3 - is-shared-array-buffer: 1.0.4 - is-string: 1.1.1 - is-typed-array: 1.1.15 - is-weakref: 1.1.1 - math-intrinsics: 1.1.0 - object-inspect: 1.13.4 - object-keys: 1.1.1 - object.assign: 4.1.7 - own-keys: 1.0.1 - regexp.prototype.flags: 1.5.4 - safe-array-concat: 1.1.4 - safe-push-apply: 1.0.0 - safe-regex-test: 1.1.0 - set-proto: 1.0.0 - stop-iteration-iterator: 1.1.0 - string.prototype.trim: 1.2.11 - string.prototype.trimend: 1.0.10 - string.prototype.trimstart: 1.0.8 - typed-array-buffer: 1.0.3 - typed-array-byte-length: 1.0.3 - typed-array-byte-offset: 1.0.4 - typed-array-length: 1.0.8 - unbox-primitive: 1.1.0 - which-typed-array: 1.1.22 - es-define-property@1.0.1: {} es-errors@1.3.0: {} - es-module-lexer@2.3.0: {} + es-module-lexer@2.3.2: {} es-object-atoms@1.1.2: dependencies: @@ -8898,18 +8412,7 @@ snapshots: has-tostringtag: 1.0.2 hasown: 2.0.4 - es-shim-unscopables@1.1.0: - dependencies: - hasown: 2.0.4 - - es-to-primitive@1.3.4: - dependencies: - es-abstract-get: 1.0.0 - es-define-property: 1.0.1 - es-errors: 1.3.0 - is-callable: 1.2.7 - is-date-object: 1.1.0 - is-symbol: 1.1.1 + es-toolkit@1.51.0: {} escalade@3.2.0: {} @@ -8919,67 +8422,21 @@ snapshots: escape-string-regexp@4.0.0: {} - eslint-import-resolver-node@0.3.10: + eslint-plugin-simple-import-sort@14.0.0(eslint@10.9.1(jiti@2.7.0)): dependencies: - debug: 3.2.7 - is-core-module: 2.16.2 - resolve: 2.0.0-next.7 - transitivePeerDependencies: - - supports-color + eslint: 10.9.1(jiti@2.7.0) - eslint-module-utils@2.14.0(@typescript-eslint/parser@8.65.0(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint@10.8.0(jiti@2.7.0)): + eslint-plugin-unused-imports@4.4.1(@typescript-eslint/eslint-plugin@8.68.0(@typescript-eslint/parser@8.68.0(eslint@10.9.1(jiti@2.7.0))(typescript@6.0.3))(eslint@10.9.1(jiti@2.7.0))(typescript@6.0.3))(eslint@10.9.1(jiti@2.7.0)): dependencies: - debug: 3.2.7 + eslint: 10.9.1(jiti@2.7.0) optionalDependencies: - '@typescript-eslint/parser': 8.65.0(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3) - eslint: 10.8.0(jiti@2.7.0) - eslint-import-resolver-node: 0.3.10 - transitivePeerDependencies: - - supports-color - - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.65.0(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.8.0(jiti@2.7.0)): - dependencies: - '@rtsao/scc': 1.1.0 - array-includes: 3.1.9 - array.prototype.findlastindex: 1.2.6 - array.prototype.flat: 1.3.3 - array.prototype.flatmap: 1.3.3 - debug: 3.2.7 - doctrine: 2.1.0 - eslint: 10.8.0(jiti@2.7.0) - eslint-import-resolver-node: 0.3.10 - eslint-module-utils: 2.14.0(@typescript-eslint/parser@8.65.0(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint@10.8.0(jiti@2.7.0)) - hasown: 2.0.4 - is-core-module: 2.16.2 - is-glob: 4.0.3 - minimatch: 3.1.5 - object.fromentries: 2.0.8 - object.groupby: 1.0.3 - object.values: 1.2.1 - semver: 6.3.1 - string.prototype.trimend: 1.0.10 - tsconfig-paths: 3.15.0 - optionalDependencies: - '@typescript-eslint/parser': 8.65.0(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3) - transitivePeerDependencies: - - eslint-import-resolver-typescript - - eslint-import-resolver-webpack - - supports-color - - eslint-plugin-simple-import-sort@14.0.0(eslint@10.8.0(jiti@2.7.0)): - dependencies: - eslint: 10.8.0(jiti@2.7.0) - - eslint-plugin-unused-imports@4.4.1(@typescript-eslint/eslint-plugin@8.65.0(@typescript-eslint/parser@8.65.0(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.8.0(jiti@2.7.0)): - dependencies: - eslint: 10.8.0(jiti@2.7.0) - optionalDependencies: - '@typescript-eslint/eslint-plugin': 8.65.0(@typescript-eslint/parser@8.65.0(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/eslint-plugin': 8.68.0(@typescript-eslint/parser@8.68.0(eslint@10.9.1(jiti@2.7.0))(typescript@6.0.3))(eslint@10.9.1(jiti@2.7.0))(typescript@6.0.3) eslint-scope@5.1.1: dependencies: esrecurse: 4.3.0 estraverse: 4.3.0 + optional: true eslint-scope@9.1.2: dependencies: @@ -8992,9 +8449,9 @@ snapshots: eslint-visitor-keys@5.0.1: {} - eslint@10.8.0(jiti@2.7.0): + eslint@10.9.1(jiti@2.7.0): dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.8.0(jiti@2.7.0)) + '@eslint-community/eslint-utils': 4.10.1(eslint@10.9.1(jiti@2.7.0)) '@eslint-community/regexpp': 4.12.2 '@eslint/config-array': 0.23.5 '@eslint/config-helpers': 0.7.0 @@ -9021,7 +8478,7 @@ snapshots: imurmurhash: 0.1.4 is-glob: 4.0.3 json-stable-stringify-without-jsonify: 1.0.1 - minimatch: 10.2.5 + minimatch: 10.2.6 natural-compare: 1.4.0 optionator: 0.9.4 optionalDependencies: @@ -9031,8 +8488,8 @@ snapshots: espree@11.2.0: dependencies: - acorn: 8.17.0 - acorn-jsx: 5.3.2(acorn@8.17.0) + acorn: 8.18.0 + acorn-jsx: 5.3.2(acorn@8.18.0) eslint-visitor-keys: 5.0.1 esprima@4.0.1: {} @@ -9045,7 +8502,8 @@ snapshots: dependencies: estraverse: 5.3.0 - estraverse@4.3.0: {} + estraverse@4.3.0: + optional: true estraverse@5.3.0: {} @@ -9057,7 +8515,7 @@ snapshots: events-universal@1.0.1: dependencies: - bare-events: 2.9.1 + bare-events: 2.9.2 transitivePeerDependencies: - bare-abort-controller @@ -9077,14 +8535,14 @@ snapshots: exit-x@0.2.2: {} - expect@30.4.1: + expect@30.5.0: dependencies: - '@jest/expect-utils': 30.4.1 - '@jest/get-type': 30.1.0 - jest-matcher-utils: 30.4.1 - jest-message-util: 30.4.1 - jest-mock: 30.4.1 - jest-util: 30.4.1 + '@jest/expect-utils': 30.5.0 + '@jest/get-type': 30.5.0 + jest-matcher-utils: 30.5.0 + jest-message-util: 30.5.0 + jest-mock: 30.5.0 + jest-util: 30.5.0 express@5.2.1: dependencies: @@ -9108,7 +8566,7 @@ snapshots: once: 1.4.0 parseurl: 1.3.3 proxy-addr: 2.0.7 - qs: 6.15.3 + qs: 6.16.0 range-parser: 1.3.0 router: 2.2.0 send: 1.2.1 @@ -9119,7 +8577,7 @@ snapshots: transitivePeerDependencies: - supports-color - exsolve@1.1.0: {} + exsolve@1.1.1: {} extend@3.0.2: {} @@ -9151,11 +8609,21 @@ snapshots: fast-safe-stringify@2.1.1: {} - fast-uri@3.1.4: {} + fast-string-truncated-width@3.0.3: {} + + fast-string-width@3.0.2: + dependencies: + fast-string-truncated-width: 3.0.3 + + fast-uri@3.1.5: {} + + fast-wrap-ansi@0.2.2: + dependencies: + fast-string-width: 3.0.2 fast-write-atomic@0.2.1: {} - fastq@1.20.1: + fastq@1.20.3: dependencies: reusify: 1.1.0 @@ -9163,9 +8631,9 @@ snapshots: dependencies: bser: 2.1.1 - fdir@6.5.0(picomatch@4.0.5): + fdir@6.5.0(picomatch@4.0.7): optionalDependencies: - picomatch: 4.0.5 + picomatch: 4.0.7 fetch-blob@3.2.0: dependencies: @@ -9176,7 +8644,7 @@ snapshots: dependencies: flat-cache: 4.0.1 - file-type@21.3.4: + file-type@22.0.2: dependencies: '@tokenizer/inflate': 0.4.1 strtok3: 10.3.5 @@ -9206,7 +8674,7 @@ snapshots: make-dir: 3.1.0 pkg-dir: 4.2.0 - find-my-way@9.7.0: + find-my-way@9.9.0: dependencies: fast-deep-equal: 3.1.3 fast-querystring: 1.1.2 @@ -9224,28 +8692,24 @@ snapshots: flat-cache@4.0.1: dependencies: - flatted: 3.4.2 + flatted: 3.4.4 keyv: 4.5.4 - flatted@3.4.2: {} + flatted@3.4.4: {} follow-redirects@1.16.0: {} - for-each@0.3.5: - dependencies: - is-callable: 1.2.7 - foreground-child@3.3.1: dependencies: cross-spawn: 7.0.6 signal-exit: 4.1.0 - fork-ts-checker-webpack-plugin@9.1.0(typescript@5.9.3)(webpack@5.106.2): + fork-ts-checker-webpack-plugin@9.1.0(typescript@6.0.3)(webpack@5.106.2): dependencies: '@babel/code-frame': 7.29.7 chalk: 4.1.2 chokidar: 4.0.3 - cosmiconfig: 8.3.6(typescript@5.9.3) + cosmiconfig: 8.3.6(typescript@6.0.3) deepmerge: 4.3.1 fs-extra: 10.1.0 memfs: 3.5.3 @@ -9254,8 +8718,9 @@ snapshots: schema-utils: 3.3.0 semver: 7.8.5 tapable: 2.3.3 - typescript: 5.9.3 + typescript: 6.0.3 webpack: 5.106.2 + optional: true form-data@4.0.6: dependencies: @@ -9279,10 +8744,10 @@ snapshots: fp-ts@2.16.0: {} - framer-motion@12.43.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8): + framer-motion@13.1.1(react-dom@19.2.8(react@19.2.8))(react@19.2.8): dependencies: - motion-dom: 12.43.0 - motion-utils: 12.39.0 + motion-dom: 13.1.1 + motion-utils: 13.0.0 tslib: 2.8.1 optionalDependencies: react: 19.2.8 @@ -9297,6 +8762,7 @@ snapshots: graceful-fs: 4.2.11 jsonfile: 6.2.1 universalify: 2.0.1 + optional: true fs-extra@11.1.1: dependencies: @@ -9308,29 +8774,13 @@ snapshots: dependencies: minimatch: 5.1.9 - fs-monkey@1.1.0: {} + fs-monkey@1.1.0: + optional: true fs.realpath@1.0.0: {} - fsevents@2.3.3: - optional: true - function-bind@1.1.2: {} - function.prototype.name@1.2.0: - dependencies: - call-bind: 1.0.9 - call-bound: 1.0.4 - es-define-property: 1.0.1 - es-errors: 1.3.0 - functions-have-names: 1.2.3 - has-property-descriptors: 1.0.2 - hasown: 2.0.4 - is-callable: 1.2.7 - is-document.all: 1.0.0 - - functions-have-names@1.2.3: {} - gaxios@7.1.3: dependencies: extend: 3.0.2 @@ -9340,7 +8790,7 @@ snapshots: transitivePeerDependencies: - supports-color - gaxios@7.2.0: + gaxios@7.3.1: dependencies: extend: 3.0.2 https-proxy-agent: 7.0.6 @@ -9350,7 +8800,7 @@ snapshots: gcp-metadata@8.1.2: dependencies: - gaxios: 7.2.0 + gaxios: 7.3.1 google-logging-utils: 1.1.3 json-bigint: 1.0.0 transitivePeerDependencies: @@ -9360,12 +8810,12 @@ snapshots: dependencies: is-property: 1.0.2 - generator-function@2.0.1: {} - gensync@1.0.0-beta.2: {} get-caller-file@2.0.5: {} + get-east-asian-width@1.6.0: {} + get-intrinsic@1.3.0: dependencies: call-bind-apply-helpers: 1.0.2 @@ -9390,12 +8840,6 @@ snapshots: get-stream@6.0.1: {} - get-symbol-description@1.1.0: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - giget@3.3.1: {} glob-parent@5.1.2: @@ -9406,7 +8850,8 @@ snapshots: dependencies: is-glob: 4.0.3 - glob-to-regexp@0.4.1: {} + glob-to-regexp@0.4.1: + optional: true glob@10.5.0: dependencies: @@ -9419,7 +8864,7 @@ snapshots: glob@13.0.6: dependencies: - minimatch: 10.2.5 + minimatch: 10.2.6 minipass: 7.1.3 path-scurry: 2.0.2 @@ -9444,11 +8889,6 @@ snapshots: dependencies: ini: 2.0.0 - globalthis@1.0.4: - dependencies: - define-properties: 1.2.1 - gopd: 1.2.0 - globby@11.1.0: dependencies: array-union: 2.1.0 @@ -9462,7 +8902,7 @@ snapshots: dependencies: base64-js: 1.5.1 ecdsa-sig-formatter: 1.0.11 - gaxios: 7.2.0 + gaxios: 7.3.1 gcp-metadata: 8.1.2 google-logging-utils: 1.1.3 jws: 4.0.1 @@ -9471,21 +8911,21 @@ snapshots: google-logging-utils@1.1.3: {} - googleapis-common@8.0.2: + googleapis-common@8.0.3: dependencies: extend: 3.0.2 gaxios: 7.1.3 google-auth-library: 10.9.0 google-logging-utils: 1.1.3 - qs: 6.15.3 + qs: 6.16.0 url-template: 2.0.8 transitivePeerDependencies: - supports-color - googleapis@173.0.0: + googleapis@176.0.0: dependencies: google-auth-library: 10.9.0 - googleapis-common: 8.0.2 + googleapis-common: 8.0.3 transitivePeerDependencies: - supports-color @@ -9506,18 +8946,8 @@ snapshots: optionalDependencies: uglify-js: 3.19.3 - has-bigints@1.1.0: {} - has-flag@4.0.0: {} - has-property-descriptors@1.0.2: - dependencies: - es-define-property: 1.0.1 - - has-proto@1.2.0: - dependencies: - dunder-proto: 1.0.1 - has-symbols@1.1.0: {} has-tostringtag@1.0.2: @@ -9591,12 +9021,13 @@ snapshots: ignore@5.3.2: {} - ignore@7.0.5: {} + ignore@7.0.7: {} import-fresh@3.3.1: dependencies: parent-module: 1.0.1 resolve-from: 4.0.0 + optional: true import-local@3.2.0: dependencies: @@ -9616,108 +9047,40 @@ snapshots: ini@2.0.0: {} - internal-slot@1.1.0: - dependencies: - es-errors: 1.3.0 - hasown: 2.0.4 - side-channel: 1.1.1 - internmap@2.0.3: {} - ioredis@5.11.1: + ioredis@6.0.0: dependencies: - '@ioredis/commands': 1.10.0 + '@ioredis/commands': 2.0.0 cluster-key-slot: 1.1.1 debug: 4.4.3 denque: 2.1.0 redis-errors: 1.2.0 - redis-parser: 3.0.0 standard-as-callback: 2.1.0 transitivePeerDependencies: - supports-color ipaddr.js@1.9.1: {} - is-array-buffer@3.0.5: - dependencies: - call-bind: 1.0.9 - call-bound: 1.0.4 - get-intrinsic: 1.3.0 - is-arrayish@0.2.1: {} - is-async-function@2.1.1: - dependencies: - async-function: 1.0.0 - call-bound: 1.0.4 - get-proto: 1.0.1 - has-tostringtag: 1.0.2 - safe-regex-test: 1.1.0 - - is-bigint@1.1.0: - dependencies: - has-bigints: 1.1.0 - - is-boolean-object@1.2.2: - dependencies: - call-bound: 1.0.4 - has-tostringtag: 1.0.2 - - is-callable@1.2.7: {} - is-core-module@2.16.2: dependencies: hasown: 2.0.4 - is-data-view@1.0.2: - dependencies: - call-bound: 1.0.4 - get-intrinsic: 1.3.0 - is-typed-array: 1.1.15 - - is-date-object@1.1.0: - dependencies: - call-bound: 1.0.4 - has-tostringtag: 1.0.2 - is-docker@2.2.1: {} - is-document.all@1.0.0: - dependencies: - call-bound: 1.0.4 - is-extglob@2.1.1: {} - is-finalizationregistry@1.1.1: - dependencies: - call-bound: 1.0.4 - is-fullwidth-code-point@3.0.0: {} is-generator-fn@2.1.0: {} - is-generator-function@1.1.2: - dependencies: - call-bound: 1.0.4 - generator-function: 2.0.1 - get-proto: 1.0.1 - has-tostringtag: 1.0.2 - safe-regex-test: 1.1.0 - is-glob@4.0.3: dependencies: is-extglob: 2.1.1 - is-interactive@1.0.0: {} - - is-map@2.0.3: {} - - is-negative-zero@2.0.3: {} - - is-number-object@1.1.1: - dependencies: - call-bound: 1.0.4 - has-tostringtag: 1.0.2 + is-interactive@2.0.0: {} is-number@7.0.0: {} @@ -9729,50 +9092,11 @@ snapshots: is-property@1.0.2: {} - is-regex@1.2.1: - dependencies: - call-bound: 1.0.4 - gopd: 1.2.0 - has-tostringtag: 1.0.2 - hasown: 2.0.4 - - is-set@2.0.3: {} - - is-shared-array-buffer@1.0.4: - dependencies: - call-bound: 1.0.4 - is-stream@2.0.1: {} is-stream@4.0.1: {} - is-string@1.1.1: - dependencies: - call-bound: 1.0.4 - has-tostringtag: 1.0.2 - - is-symbol@1.1.1: - dependencies: - call-bound: 1.0.4 - has-symbols: 1.1.0 - safe-regex-test: 1.1.0 - - is-typed-array@1.1.15: - dependencies: - which-typed-array: 1.1.22 - - is-unicode-supported@0.1.0: {} - - is-weakmap@2.0.2: {} - - is-weakref@1.1.1: - dependencies: - call-bound: 1.0.4 - - is-weakset@2.0.4: - dependencies: - call-bound: 1.0.4 - get-intrinsic: 1.3.0 + is-unicode-supported@2.1.0: {} is-windows@1.0.2: {} @@ -9782,8 +9106,6 @@ snapshots: isarray@1.0.0: {} - isarray@2.0.5: {} - isexe@2.0.0: {} istanbul-lib-coverage@3.2.2: {} @@ -9791,7 +9113,7 @@ snapshots: istanbul-lib-instrument@6.0.3: dependencies: '@babel/core': 7.29.7 - '@babel/parser': 7.29.7 + '@babel/parser': 7.29.8 '@istanbuljs/schema': 0.1.6 istanbul-lib-coverage: 3.2.2 semver: 7.8.5 @@ -9825,31 +9147,31 @@ snapshots: optionalDependencies: '@pkgjs/parseargs': 0.11.0 - jest-changed-files@30.4.1: + jest-changed-files@30.5.0: dependencies: execa: 5.1.1 - jest-util: 30.4.1 + jest-util: 30.5.0 p-limit: 3.1.0 - jest-circus@30.4.2: + jest-circus@30.5.0: dependencies: - '@jest/environment': 30.4.1 - '@jest/expect': 30.4.1 - '@jest/test-result': 30.4.1 - '@jest/types': 30.4.1 - '@types/node': 26.1.2 + '@jest/environment': 30.5.0 + '@jest/expect': 30.5.0 + '@jest/test-result': 30.5.0 + '@jest/types': 30.5.0 + '@types/node': 26.4.0 chalk: 4.1.2 co: 4.6.0 dedent: 1.7.2 is-generator-fn: 2.1.0 - jest-each: 30.4.1 - jest-matcher-utils: 30.4.1 - jest-message-util: 30.4.1 - jest-runtime: 30.4.2 - jest-snapshot: 30.4.1 - jest-util: 30.4.1 + jest-each: 30.5.0 + jest-matcher-utils: 30.5.0 + jest-message-util: 30.5.0 + jest-runtime: 30.5.0 + jest-snapshot: 30.5.0 + jest-util: 30.5.0 p-limit: 3.1.0 - pretty-format: 30.4.1 + pretty-format: 30.5.0 pure-rand: 7.0.1 slash: 3.0.0 stack-utils: 2.0.6 @@ -9857,17 +9179,17 @@ snapshots: - babel-plugin-macros - supports-color - jest-cli@30.4.2(@types/node@26.1.2)(ts-node@10.9.2(@types/node@26.1.2)(typescript@6.0.3)): + jest-cli@30.5.0(@types/node@26.4.0)(ts-node@10.9.2(@types/node@26.4.0)(typescript@6.0.3)): dependencies: - '@jest/core': 30.4.2(ts-node@10.9.2(@types/node@26.1.2)(typescript@6.0.3)) - '@jest/test-result': 30.4.1 - '@jest/types': 30.4.1 + '@jest/core': 30.5.0(ts-node@10.9.2(@types/node@26.4.0)(typescript@6.0.3)) + '@jest/test-result': 30.5.0 + '@jest/types': 30.5.0 chalk: 4.1.2 exit-x: 0.2.2 import-local: 3.2.0 - jest-config: 30.4.2(@types/node@26.1.2)(ts-node@10.9.2(@types/node@26.1.2)(typescript@6.0.3)) - jest-util: 30.4.1 - jest-validate: 30.4.1 + jest-config: 30.5.0(@types/node@26.4.0)(ts-node@10.9.2(@types/node@26.4.0)(typescript@6.0.3)) + jest-util: 30.5.0 + jest-validate: 30.5.0 yargs: 17.7.3 transitivePeerDependencies: - '@types/node' @@ -9876,266 +9198,263 @@ snapshots: - supports-color - ts-node - jest-config@30.4.2(@types/node@26.1.2)(ts-node@10.9.2(@types/node@26.1.2)(typescript@6.0.3)): + jest-config@30.5.0(@types/node@26.4.0)(ts-node@10.9.2(@types/node@26.4.0)(typescript@6.0.3)): dependencies: '@babel/core': 7.29.7 - '@jest/get-type': 30.1.0 - '@jest/pattern': 30.4.0 - '@jest/test-sequencer': 30.4.1 - '@jest/types': 30.4.1 - babel-jest: 30.4.1(@babel/core@7.29.7) + '@jest/get-type': 30.5.0 + '@jest/pattern': 30.5.0 + '@jest/test-sequencer': 30.5.0 + '@jest/types': 30.5.0 + babel-jest: 30.5.0(@babel/core@7.29.7) chalk: 4.1.2 ci-info: 4.4.0 deepmerge: 4.3.1 - glob: 10.5.0 + glob: 13.0.6 graceful-fs: 4.2.11 - jest-circus: 30.4.2 - jest-docblock: 30.4.0 - jest-environment-node: 30.4.1 - jest-regex-util: 30.4.0 - jest-resolve: 30.4.1 - jest-runner: 30.4.2 - jest-util: 30.4.1 - jest-validate: 30.4.1 + jest-circus: 30.5.0 + jest-docblock: 30.5.0 + jest-environment-node: 30.5.0 + jest-regex-util: 30.5.0 + jest-resolve: 30.5.0 + jest-runner: 30.5.0 + jest-util: 30.5.0 + jest-validate: 30.5.0 parse-json: 5.2.0 - pretty-format: 30.4.1 + pretty-format: 30.5.0 slash: 3.0.0 strip-json-comments: 3.1.1 optionalDependencies: - '@types/node': 26.1.2 - ts-node: 10.9.2(@types/node@26.1.2)(typescript@6.0.3) + '@types/node': 26.4.0 + ts-node: 10.9.2(@types/node@26.4.0)(typescript@6.0.3) transitivePeerDependencies: - babel-plugin-macros - supports-color - jest-diff@30.4.1: + jest-diff@30.5.0: dependencies: - '@jest/diff-sequences': 30.4.0 - '@jest/get-type': 30.1.0 + '@jest/diff-sequences': 30.5.0 + '@jest/get-type': 30.5.0 chalk: 4.1.2 - pretty-format: 30.4.1 + pretty-format: 30.5.0 - jest-docblock@30.4.0: + jest-docblock@30.5.0: dependencies: detect-newline: 3.1.0 - jest-each@30.4.1: + jest-each@30.5.0: dependencies: - '@jest/get-type': 30.1.0 - '@jest/types': 30.4.1 + '@jest/get-type': 30.5.0 + '@jest/types': 30.5.0 chalk: 4.1.2 - jest-util: 30.4.1 - pretty-format: 30.4.1 + jest-util: 30.5.0 + pretty-format: 30.5.0 - jest-environment-node@30.4.1: + jest-environment-node@30.5.0: dependencies: - '@jest/environment': 30.4.1 - '@jest/fake-timers': 30.4.1 - '@jest/types': 30.4.1 - '@types/node': 26.1.2 - jest-mock: 30.4.1 - jest-util: 30.4.1 - jest-validate: 30.4.1 + '@jest/environment': 30.5.0 + '@jest/fake-timers': 30.5.0 + '@jest/types': 30.5.0 + '@types/node': 26.4.0 + jest-mock: 30.5.0 + jest-util: 30.5.0 + jest-validate: 30.5.0 - jest-haste-map@30.4.1: + jest-haste-map@30.5.0: dependencies: - '@jest/types': 30.4.1 - '@types/node': 26.1.2 + '@jest/types': 30.5.0 + '@parcel/watcher': 2.6.0 + '@types/node': 26.4.0 anymatch: 3.1.3 fb-watchman: 2.0.2 + fdir: 6.5.0(picomatch@4.0.7) graceful-fs: 4.2.11 - jest-regex-util: 30.4.0 - jest-util: 30.4.1 - jest-worker: 30.4.1 - picomatch: 4.0.5 - walker: 1.0.8 - optionalDependencies: - fsevents: 2.3.3 + jest-regex-util: 30.5.0 + jest-util: 30.5.0 + jest-worker: 30.5.0 + picomatch: 4.0.7 - jest-leak-detector@30.4.1: + jest-leak-detector@30.5.0: dependencies: - '@jest/get-type': 30.1.0 - pretty-format: 30.4.1 + '@jest/get-type': 30.5.0 + pretty-format: 30.5.0 - jest-matcher-utils@30.4.1: + jest-matcher-utils@30.5.0: dependencies: - '@jest/get-type': 30.1.0 + '@jest/get-type': 30.5.0 chalk: 4.1.2 - jest-diff: 30.4.1 - pretty-format: 30.4.1 + jest-diff: 30.5.0 + pretty-format: 30.5.0 - jest-message-util@30.4.1: + jest-message-util@30.5.0: dependencies: '@babel/code-frame': 7.29.7 - '@jest/types': 30.4.1 + '@jest/types': 30.5.0 '@types/stack-utils': 2.0.3 chalk: 4.1.2 graceful-fs: 4.2.11 - jest-util: 30.4.1 - picomatch: 4.0.5 - pretty-format: 30.4.1 + jest-util: 30.5.0 + picomatch: 4.0.7 + pretty-format: 30.5.0 slash: 3.0.0 stack-utils: 2.0.6 - jest-mock@30.4.1: + jest-mock@30.5.0: dependencies: - '@jest/types': 30.4.1 - '@types/node': 26.1.2 - jest-util: 30.4.1 - - jest-pnp-resolver@1.2.3(jest-resolve@30.4.1): - optionalDependencies: - jest-resolve: 30.4.1 + '@jest/expect-utils': 30.5.0 + '@jest/types': 30.5.0 + '@types/node': 26.4.0 + jest-util: 30.5.0 - jest-regex-util@30.4.0: {} + jest-regex-util@30.5.0: {} - jest-resolve-dependencies@30.4.2: + jest-resolve-dependencies@30.5.0: dependencies: - jest-regex-util: 30.4.0 - jest-snapshot: 30.4.1 + jest-regex-util: 30.5.0 + jest-snapshot: 30.5.0 transitivePeerDependencies: - supports-color - jest-resolve@30.4.1: + jest-resolve@30.5.0: dependencies: chalk: 4.1.2 graceful-fs: 4.2.11 - jest-haste-map: 30.4.1 - jest-pnp-resolver: 1.2.3(jest-resolve@30.4.1) - jest-util: 30.4.1 - jest-validate: 30.4.1 + jest-haste-map: 30.5.0 + jest-util: 30.5.0 + jest-validate: 30.5.0 slash: 3.0.0 unrs-resolver: 1.12.2 - jest-runner@30.4.2: + jest-runner@30.5.0: dependencies: - '@jest/console': 30.4.1 - '@jest/environment': 30.4.1 - '@jest/test-result': 30.4.1 - '@jest/transform': 30.4.1 - '@jest/types': 30.4.1 - '@types/node': 26.1.2 + '@jest/console': 30.5.0 + '@jest/environment': 30.5.0 + '@jest/source-map': 30.5.0 + '@jest/test-result': 30.5.0 + '@jest/transform': 30.5.0 + '@jest/types': 30.5.0 + '@types/node': 26.4.0 chalk: 4.1.2 emittery: 0.13.1 exit-x: 0.2.2 graceful-fs: 4.2.11 - jest-docblock: 30.4.0 - jest-environment-node: 30.4.1 - jest-haste-map: 30.4.1 - jest-leak-detector: 30.4.1 - jest-message-util: 30.4.1 - jest-resolve: 30.4.1 - jest-runtime: 30.4.2 - jest-util: 30.4.1 - jest-watcher: 30.4.1 - jest-worker: 30.4.1 + jest-docblock: 30.5.0 + jest-environment-node: 30.5.0 + jest-haste-map: 30.5.0 + jest-leak-detector: 30.5.0 + jest-message-util: 30.5.0 + jest-resolve: 30.5.0 + jest-runtime: 30.5.0 + jest-util: 30.5.0 + jest-watcher: 30.5.0 + jest-worker: 30.5.0 p-limit: 3.1.0 - source-map-support: 0.5.13 transitivePeerDependencies: - supports-color - jest-runtime@30.4.2: + jest-runtime@30.5.0: dependencies: - '@jest/environment': 30.4.1 - '@jest/fake-timers': 30.4.1 - '@jest/globals': 30.4.1 - '@jest/source-map': 30.0.1 - '@jest/test-result': 30.4.1 - '@jest/transform': 30.4.1 - '@jest/types': 30.4.1 - '@types/node': 26.1.2 + '@jest/environment': 30.5.0 + '@jest/fake-timers': 30.5.0 + '@jest/globals': 30.5.0 + '@jest/source-map': 30.5.0 + '@jest/test-result': 30.5.0 + '@jest/transform': 30.5.0 + '@jest/types': 30.5.0 + '@types/node': 26.4.0 chalk: 4.1.2 - cjs-module-lexer: 2.2.0 + cjs-module-lexer: 2.2.1 collect-v8-coverage: 1.0.3 - glob: 10.5.0 + es-module-lexer: 2.3.2 + glob: 13.0.6 graceful-fs: 4.2.11 - jest-haste-map: 30.4.1 - jest-message-util: 30.4.1 - jest-mock: 30.4.1 - jest-regex-util: 30.4.0 - jest-resolve: 30.4.1 - jest-snapshot: 30.4.1 - jest-util: 30.4.1 + jest-haste-map: 30.5.0 + jest-message-util: 30.5.0 + jest-mock: 30.5.0 + jest-regex-util: 30.5.0 + jest-resolve: 30.5.0 + jest-snapshot: 30.5.0 + jest-util: 30.5.0 slash: 3.0.0 strip-bom: 4.0.0 transitivePeerDependencies: - supports-color - jest-snapshot@30.4.1: + jest-snapshot@30.5.0: dependencies: '@babel/core': 7.29.7 - '@babel/generator': 7.29.7 + '@babel/generator': 7.29.8 '@babel/plugin-syntax-jsx': 7.29.7(@babel/core@7.29.7) '@babel/plugin-syntax-typescript': 7.29.7(@babel/core@7.29.7) - '@babel/types': 7.29.7 - '@jest/expect-utils': 30.4.1 - '@jest/get-type': 30.1.0 - '@jest/snapshot-utils': 30.4.1 - '@jest/transform': 30.4.1 - '@jest/types': 30.4.1 + '@babel/types': 7.29.8 + '@jest/expect-utils': 30.5.0 + '@jest/get-type': 30.5.0 + '@jest/snapshot-utils': 30.5.0 + '@jest/transform': 30.5.0 + '@jest/types': 30.5.0 babel-preset-current-node-syntax: 1.2.0(@babel/core@7.29.7) chalk: 4.1.2 - expect: 30.4.1 + expect: 30.5.0 graceful-fs: 4.2.11 - jest-diff: 30.4.1 - jest-matcher-utils: 30.4.1 - jest-message-util: 30.4.1 - jest-util: 30.4.1 - pretty-format: 30.4.1 + jest-diff: 30.5.0 + jest-matcher-utils: 30.5.0 + jest-message-util: 30.5.0 + jest-util: 30.5.0 + pretty-format: 30.5.0 semver: 7.8.5 synckit: 0.11.13 transitivePeerDependencies: - supports-color - jest-util@30.4.1: + jest-util@30.5.0: dependencies: - '@jest/types': 30.4.1 - '@types/node': 26.1.2 + '@jest/types': 30.5.0 + '@types/node': 26.4.0 chalk: 4.1.2 ci-info: 4.4.0 graceful-fs: 4.2.11 - picomatch: 4.0.5 + picomatch: 4.0.7 - jest-validate@30.4.1: + jest-validate@30.5.0: dependencies: - '@jest/get-type': 30.1.0 - '@jest/types': 30.4.1 + '@jest/get-type': 30.5.0 + '@jest/types': 30.5.0 camelcase: 6.3.0 chalk: 4.1.2 leven: 3.1.0 - pretty-format: 30.4.1 + pretty-format: 30.5.0 - jest-watcher@30.4.1: + jest-watcher@30.5.0: dependencies: - '@jest/test-result': 30.4.1 - '@jest/types': 30.4.1 - '@types/node': 26.1.2 + '@jest/test-result': 30.5.0 + '@jest/types': 30.5.0 + '@types/node': 26.4.0 ansi-escapes: 4.3.2 chalk: 4.1.2 emittery: 0.13.1 - jest-util: 30.4.1 + jest-util: 30.5.0 string-length: 4.0.2 jest-worker@27.5.1: dependencies: - '@types/node': 26.1.2 + '@types/node': 26.4.0 merge-stream: 2.0.0 supports-color: 8.1.1 + optional: true - jest-worker@30.4.1: + jest-worker@30.5.0: dependencies: - '@types/node': 26.1.2 - '@ungap/structured-clone': 1.3.2 - jest-util: 30.4.1 + '@types/node': 26.4.0 + '@ungap/structured-clone': 1.4.0 + jest-util: 30.5.0 merge-stream: 2.0.0 supports-color: 8.1.1 - jest@30.4.2(@types/node@26.1.2)(ts-node@10.9.2(@types/node@26.1.2)(typescript@6.0.3)): + jest@30.5.0(@types/node@26.4.0)(ts-node@10.9.2(@types/node@26.4.0)(typescript@6.0.3)): dependencies: - '@jest/core': 30.4.2(ts-node@10.9.2(@types/node@26.1.2)(typescript@6.0.3)) - '@jest/types': 30.4.1 + '@jest/core': 30.5.0(ts-node@10.9.2(@types/node@26.4.0)(typescript@6.0.3)) + '@jest/types': 30.5.0 import-local: 3.2.0 - jest-cli: 30.4.2(@types/node@26.1.2)(ts-node@10.9.2(@types/node@26.1.2)(typescript@6.0.3)) + jest-cli: 30.5.0(@types/node@26.4.0)(ts-node@10.9.2(@types/node@26.4.0)(typescript@6.0.3)) transitivePeerDependencies: - '@types/node' - babel-plugin-macros @@ -10147,14 +9466,15 @@ snapshots: js-tokens@4.0.0: {} - js-yaml@3.15.0: + js-yaml@3.15.2: dependencies: argparse: 1.0.10 esprima: 4.0.1 - js-yaml@4.3.0: + js-yaml@4.3.2: dependencies: argparse: 2.0.1 + optional: true js-yaml@5.2.2: dependencies: @@ -10176,10 +9496,6 @@ snapshots: json-stable-stringify-without-jsonify@1.0.1: {} - json5@1.0.2: - dependencies: - minimist: 1.2.8 - json5@2.2.3: {} jsonc-parser@3.3.1: {} @@ -10233,7 +9549,7 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 - libphonenumber-js@1.13.8: {} + libphonenumber-js@1.13.12: {} lightningcss-android-arm64@1.32.0: optional: true @@ -10288,7 +9604,8 @@ snapshots: load-esm@1.0.3: {} - loader-runner@4.3.2: {} + loader-runner@4.3.2: + optional: true locate-path@5.0.0: dependencies: @@ -10324,16 +9641,16 @@ snapshots: lodash@4.18.1: {} - log-symbols@4.1.0: + log-symbols@7.0.1: dependencies: - chalk: 4.1.2 - is-unicode-supported: 0.1.0 + is-unicode-supported: 2.1.0 + yoctocolors: 2.2.0 long@5.3.2: {} lru-cache@10.4.3: {} - lru-cache@11.5.1: {} + lru-cache@11.5.2: {} lru-cache@5.1.1: dependencies: @@ -10341,17 +9658,17 @@ snapshots: lru.min@1.1.4: {} - lucide-react@1.28.0(react@19.2.8): + lucide-react@1.38.0(react@19.2.8): dependencies: react: 19.2.8 - magic-string@0.30.17: + magic-string@0.30.21: dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/sourcemap-codec': 1.6.0 - magic-string@0.30.21: + magic-string@1.0.0: dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/sourcemap-codec': 1.6.0 make-dir@3.1.0: dependencies: @@ -10363,19 +9680,16 @@ snapshots: make-error@1.3.6: {} - makeerror@1.0.12: - dependencies: - tmpl: 1.0.5 - math-intrinsics@1.1.0: {} media-typer@0.3.0: {} - media-typer@1.1.0: {} + media-typer@1.1.1: {} memfs@3.5.3: dependencies: fs-monkey: 1.1.0 + optional: true merge-descriptors@2.0.0: {} @@ -10406,33 +9720,43 @@ snapshots: mimic-fn@2.1.0: {} + mimic-function@5.0.1: {} + min-indent@1.0.1: {} - minimatch@10.2.5: + minimatch@10.2.6: dependencies: - brace-expansion: 5.0.8 + brace-expansion: 5.0.9 minimatch@3.1.5: dependencies: - brace-expansion: 5.0.8 + brace-expansion: 5.0.9 minimatch@5.1.9: dependencies: - brace-expansion: 5.0.8 + brace-expansion: 5.0.9 minimatch@9.0.9: dependencies: - brace-expansion: 5.0.8 + brace-expansion: 5.0.9 minimist@1.2.8: {} minipass@7.1.3: {} - motion-dom@12.43.0: + motion-dom@13.1.1: dependencies: - motion-utils: 12.39.0 + motion-utils: 13.0.0 + + motion-utils@13.0.0: {} - motion-utils@12.39.0: {} + motion@13.1.1(react-dom@19.2.8(react@19.2.8))(react@19.2.8): + dependencies: + framer-motion: 13.1.1(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + tslib: 2.8.1 + optionalDependencies: + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) ms@2.1.2: {} @@ -10445,7 +9769,7 @@ snapshots: concat-stream: 2.0.0 type-is: 1.6.18 - mute-stream@2.0.0: {} + mute-stream@3.0.0: {} mysql2@3.15.3: dependencies: @@ -10463,13 +9787,15 @@ snapshots: dependencies: lru.min: 1.1.4 - nanoid@3.3.16: {} + nanoid@3.3.18: {} napi-postinstall@0.3.4: {} natural-compare@1.4.0: {} - negotiator@1.0.0: {} + negotiator@1.1.0: + dependencies: + content-type: 2.1.0 neo-async@2.6.2: {} @@ -10480,46 +9806,45 @@ snapshots: react: 19.2.8 react-dom: 19.2.8(react@19.2.8) - next@16.2.12(@opentelemetry/api@1.4.1)(@types/node@26.1.2)(react-dom@19.2.8(react@19.2.8))(react@19.2.8): + next@16.3.3(@opentelemetry/api@1.4.1)(@types/node@26.4.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8): dependencies: - '@next/env': 16.2.12 - '@swc/helpers': 0.5.15 - baseline-browser-mapping: 2.10.42 - caniuse-lite: 1.0.30001803 - postcss: 8.5.23 + '@next/env': 16.3.3 + '@swc/helpers': 0.5.23 + baseline-browser-mapping: 2.11.20 + caniuse-lite: 1.0.30001810 + postcss: 8.5.26 react: 19.2.8 react-dom: 19.2.8(react@19.2.8) styled-jsx: 5.1.6(react@19.2.8) optionalDependencies: - '@next/swc-darwin-arm64': 16.2.12 - '@next/swc-darwin-x64': 16.2.12 - '@next/swc-linux-arm64-gnu': 16.2.12 - '@next/swc-linux-arm64-musl': 16.2.12 - '@next/swc-linux-x64-gnu': 16.2.12 - '@next/swc-linux-x64-musl': 16.2.12 - '@next/swc-win32-arm64-msvc': 16.2.12 - '@next/swc-win32-x64-msvc': 16.2.12 + '@next/swc-darwin-arm64': 16.3.3 + '@next/swc-darwin-x64': 16.3.3 + '@next/swc-linux-arm64-gnu': 16.3.3 + '@next/swc-linux-arm64-musl': 16.3.3 + '@next/swc-linux-x64-gnu': 16.3.3 + '@next/swc-linux-x64-musl': 16.3.3 + '@next/swc-win32-arm64-msvc': 16.3.3 + '@next/swc-win32-x64-msvc': 16.3.3 '@opentelemetry/api': 1.4.1 - sharp: 0.35.3(@types/node@26.1.2) + sharp: 0.35.4(@types/node@26.4.0) transitivePeerDependencies: - '@babel/core' - '@types/node' - babel-plugin-macros - node-abort-controller@3.1.1: {} + node-abort-controller@3.1.1: + optional: true + + node-addon-api@7.1.1: {} node-domexception@1.0.0: {} - node-emoji@1.11.0: - dependencies: - lodash: 4.18.1 - - node-exports-info@1.6.2: + node-emoji@2.2.0: dependencies: - array.prototype.flatmap: 1.3.3 - es-errors: 1.3.0 - object.entries: 1.1.9 - semver: 6.3.1 + '@sindresorhus/is': 4.6.0 + char-regex: 1.0.2 + emojilib: 2.4.0 + skin-tone: 2.0.0 node-fetch@2.6.11: dependencies: @@ -10537,7 +9862,7 @@ snapshots: node-int64@0.4.0: {} - node-releases@2.0.50: {} + node-releases@2.0.54: {} normalize-package-data@2.5.0: dependencies: @@ -10569,45 +9894,7 @@ snapshots: object-inspect@1.13.4: {} - object-keys@1.1.1: {} - - object.assign@4.1.7: - dependencies: - call-bind: 1.0.9 - call-bound: 1.0.4 - define-properties: 1.2.1 - es-object-atoms: 1.1.2 - has-symbols: 1.1.0 - object-keys: 1.1.1 - - object.entries@1.1.9: - dependencies: - call-bind: 1.0.9 - call-bound: 1.0.4 - define-properties: 1.2.1 - es-object-atoms: 1.1.2 - - object.fromentries@2.0.8: - dependencies: - call-bind: 1.0.9 - define-properties: 1.2.1 - es-abstract: 1.24.2 - es-object-atoms: 1.1.2 - - object.groupby@1.0.3: - dependencies: - call-bind: 1.0.9 - define-properties: 1.2.1 - es-abstract: 1.24.2 - - object.values@1.2.1: - dependencies: - call-bind: 1.0.9 - call-bound: 1.0.4 - define-properties: 1.2.1 - es-object-atoms: 1.1.2 - - ohash@2.0.11: {} + ohash@2.0.12: {} on-finished@2.4.1: dependencies: @@ -10621,6 +9908,10 @@ snapshots: dependencies: mimic-fn: 2.1.0 + onetime@7.0.0: + dependencies: + mimic-function: 5.0.1 + open@7.4.2: dependencies: is-docker: 2.2.1 @@ -10635,23 +9926,16 @@ snapshots: type-check: 0.4.0 word-wrap: 1.2.5 - ora@5.4.1: + ora@9.4.1: dependencies: - bl: 4.1.0 - chalk: 4.1.2 - cli-cursor: 3.1.0 - cli-spinners: 2.9.2 - is-interactive: 1.0.0 - is-unicode-supported: 0.1.0 - log-symbols: 4.1.0 - strip-ansi: 6.0.1 - wcwidth: 1.0.1 - - own-keys@1.0.1: - dependencies: - get-intrinsic: 1.3.0 - object-keys: 1.1.1 - safe-push-apply: 1.0.0 + chalk: 5.6.2 + cli-cursor: 5.0.0 + cli-spinners: 3.4.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.2.2 p-filter@2.1.0: dependencies: @@ -10691,6 +9975,7 @@ snapshots: parent-module@1.0.1: dependencies: callsites: 3.1.0 + optional: true parse-json@5.2.0: dependencies: @@ -10716,7 +10001,7 @@ snapshots: path-scurry@2.0.2: dependencies: - lru-cache: 11.5.1 + lru-cache: 11.5.2 minipass: 7.1.3 path-to-regexp@8.4.2: {} @@ -10734,11 +10019,11 @@ snapshots: pg-int8@1.0.1: {} - pg-pool@3.14.0(pg@8.22.0): + pg-pool@3.14.0(pg@8.23.0): dependencies: - pg: 8.22.0 + pg: 8.23.0 - pg-protocol@1.15.0: {} + pg-protocol@1.16.0: {} pg-types@2.2.0: dependencies: @@ -10748,11 +10033,11 @@ snapshots: postgres-date: 1.0.7 postgres-interval: 1.2.0 - pg@8.22.0: + pg@8.23.0: dependencies: pg-connection-string: 2.14.0 - pg-pool: 3.14.0(pg@8.22.0) - pg-protocol: 1.15.0 + pg-pool: 3.14.0(pg@8.23.0) + pg-protocol: 1.16.0 pg-types: 2.2.0 pgpass: 1.0.5 optionalDependencies: @@ -10766,10 +10051,10 @@ snapshots: picomatch@2.3.2: {} - picomatch@4.0.4: {} - picomatch@4.0.5: {} + picomatch@4.0.7: {} + pirates@4.0.7: {} pkg-dir@4.2.0: @@ -10779,16 +10064,14 @@ snapshots: pkg-types@2.3.1: dependencies: confbox: 0.2.4 - exsolve: 1.1.0 + exsolve: 1.1.1 pathe: 2.0.3 pluralize@8.0.0: {} - possible-typed-array-names@1.1.0: {} - - postcss@8.5.23: + postcss@8.5.26: dependencies: - nanoid: 3.3.16 + nanoid: 3.3.18 picocolors: 1.1.1 source-map-js: 1.2.1 @@ -10814,12 +10097,12 @@ snapshots: prettier@3.9.6: {} - pretty-format@30.4.1: + pretty-format@30.5.0: dependencies: - '@jest/schemas': 30.4.1 + '@jest/react-is-18': react-is@18.3.1 + '@jest/react-is-19': react-is@19.2.8 + '@jest/schemas': 30.5.0 ansi-styles: 5.2.0 - react-is-18: react-is@18.3.1 - react-is-19: react-is@19.2.8 prisma-dbml-generator@0.12.0: dependencies: @@ -10829,12 +10112,12 @@ snapshots: - encoding - supports-color - prisma@7.9.1(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@6.0.3): + prisma@7.10.0(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@6.0.3): dependencies: - '@prisma/config': 7.9.1 + '@prisma/config': 7.10.0 '@prisma/dev': 0.24.17(typescript@6.0.3) - '@prisma/engines': 7.9.1 - '@prisma/studio-core': 0.33.0(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@prisma/engines': 7.10.0 + '@prisma/studio-core': 0.33.0(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) mysql2: 3.15.3 postgres: 3.4.7 optionalDependencies: @@ -10876,7 +10159,7 @@ snapshots: pure-rand@7.0.1: {} - qs@6.15.3: + qs@6.16.0: dependencies: es-define-property: 1.0.1 side-channel: 1.1.1 @@ -10953,40 +10236,17 @@ snapshots: readdir-glob@3.0.0: dependencies: - minimatch: 10.2.5 + minimatch: 10.2.6 - readdirp@4.1.2: {} + readdirp@4.1.2: + optional: true - readdirp@5.0.0: {} + readdirp@5.1.1: {} redis-errors@1.2.0: {} - redis-parser@3.0.0: - dependencies: - redis-errors: 1.2.0 - reflect-metadata@0.2.2: {} - reflect.getprototypeof@1.0.10: - dependencies: - call-bind: 1.0.9 - define-properties: 1.2.1 - es-abstract: 1.24.2 - es-errors: 1.3.0 - es-object-atoms: 1.1.2 - get-intrinsic: 1.3.0 - get-proto: 1.0.1 - which-builtin-type: 1.2.1 - - regexp.prototype.flags@1.5.4: - dependencies: - call-bind: 1.0.9 - define-properties: 1.2.1 - es-errors: 1.3.0 - get-proto: 1.0.1 - gopd: 1.2.0 - set-function-name: 2.0.2 - remeda@2.33.4: {} replace-string@3.1.0: {} @@ -10999,7 +10259,8 @@ snapshots: dependencies: resolve-from: 5.0.0 - resolve-from@4.0.0: {} + resolve-from@4.0.0: + optional: true resolve-from@5.0.0: {} @@ -11009,19 +10270,10 @@ snapshots: path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 - resolve@2.0.0-next.7: - dependencies: - es-errors: 1.3.0 - is-core-module: 2.16.2 - node-exports-info: 1.6.2 - object-keys: 1.1.1 - path-parse: 1.0.7 - supports-preserve-symlinks-flag: 1.0.0 - - restore-cursor@3.1.0: + restore-cursor@5.1.0: dependencies: - onetime: 5.1.2 - signal-exit: 3.0.7 + onetime: 7.0.0 + signal-exit: 4.1.0 ret@0.5.0: {} @@ -11055,37 +10307,14 @@ snapshots: dependencies: queue-microtask: 1.2.3 - rxjs@7.8.1: - dependencies: - tslib: 2.8.1 - rxjs@7.8.2: dependencies: tslib: 2.8.1 - safe-array-concat@1.1.4: - dependencies: - call-bind: 1.0.9 - call-bound: 1.0.4 - get-intrinsic: 1.3.0 - has-symbols: 1.1.0 - isarray: 2.0.5 - safe-buffer@5.1.2: {} safe-buffer@5.2.1: {} - safe-push-apply@1.0.0: - dependencies: - es-errors: 1.3.0 - isarray: 2.0.5 - - safe-regex-test@1.1.0: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - is-regex: 1.2.1 - safe-regex2@5.1.1: dependencies: ret: 0.5.0 @@ -11099,6 +10328,7 @@ snapshots: '@types/json-schema': 7.0.15 ajv: 6.15.0 ajv-keywords: 3.5.2(ajv@6.15.0) + optional: true schema-utils@4.3.3: dependencies: @@ -11106,6 +10336,7 @@ snapshots: ajv: 8.20.0 ajv-formats: 2.1.1(ajv@8.20.0) ajv-keywords: 5.1.0(ajv@8.20.0) + optional: true semver@5.7.2: {} @@ -11142,62 +10373,40 @@ snapshots: server-only@0.0.1: {} - set-function-length@1.2.2: - dependencies: - define-data-property: 1.1.4 - es-errors: 1.3.0 - function-bind: 1.1.2 - get-intrinsic: 1.3.0 - gopd: 1.2.0 - has-property-descriptors: 1.0.2 - - set-function-name@2.0.2: - dependencies: - define-data-property: 1.1.4 - es-errors: 1.3.0 - functions-have-names: 1.2.3 - has-property-descriptors: 1.0.2 - - set-proto@1.0.0: - dependencies: - dunder-proto: 1.0.1 - es-errors: 1.3.0 - es-object-atoms: 1.1.2 - setprototypeof@1.2.0: {} - sharp@0.35.3(@types/node@26.1.2): + sharp@0.35.4(@types/node@26.4.0): dependencies: '@img/colour': 1.1.0 detect-libc: 2.1.2 semver: 7.8.5 optionalDependencies: - '@img/sharp-darwin-arm64': 0.35.3 - '@img/sharp-darwin-x64': 0.35.3 - '@img/sharp-freebsd-wasm32': 0.35.3 - '@img/sharp-libvips-darwin-arm64': 1.3.2 - '@img/sharp-libvips-darwin-x64': 1.3.2 - '@img/sharp-libvips-linux-arm': 1.3.2 - '@img/sharp-libvips-linux-arm64': 1.3.2 - '@img/sharp-libvips-linux-ppc64': 1.3.2 - '@img/sharp-libvips-linux-riscv64': 1.3.2 - '@img/sharp-libvips-linux-s390x': 1.3.2 - '@img/sharp-libvips-linux-x64': 1.3.2 - '@img/sharp-libvips-linuxmusl-arm64': 1.3.2 - '@img/sharp-libvips-linuxmusl-x64': 1.3.2 - '@img/sharp-linux-arm': 0.35.3 - '@img/sharp-linux-arm64': 0.35.3 - '@img/sharp-linux-ppc64': 0.35.3 - '@img/sharp-linux-riscv64': 0.35.3 - '@img/sharp-linux-s390x': 0.35.3 - '@img/sharp-linux-x64': 0.35.3 - '@img/sharp-linuxmusl-arm64': 0.35.3 - '@img/sharp-linuxmusl-x64': 0.35.3 - '@img/sharp-webcontainers-wasm32': 0.35.3 - '@img/sharp-win32-arm64': 0.35.3 - '@img/sharp-win32-ia32': 0.35.3 - '@img/sharp-win32-x64': 0.35.3 - '@types/node': 26.1.2 + '@img/sharp-darwin-arm64': 0.35.4 + '@img/sharp-darwin-x64': 0.35.4 + '@img/sharp-freebsd-wasm32': 0.35.4 + '@img/sharp-libvips-darwin-arm64': 1.3.3 + '@img/sharp-libvips-darwin-x64': 1.3.3 + '@img/sharp-libvips-linux-arm': 1.3.3 + '@img/sharp-libvips-linux-arm64': 1.3.3 + '@img/sharp-libvips-linux-ppc64': 1.3.3 + '@img/sharp-libvips-linux-riscv64': 1.3.3 + '@img/sharp-libvips-linux-s390x': 1.3.3 + '@img/sharp-libvips-linux-x64': 1.3.3 + '@img/sharp-libvips-linuxmusl-arm64': 1.3.3 + '@img/sharp-libvips-linuxmusl-x64': 1.3.3 + '@img/sharp-linux-arm': 0.35.4 + '@img/sharp-linux-arm64': 0.35.4 + '@img/sharp-linux-ppc64': 0.35.4 + '@img/sharp-linux-riscv64': 0.35.4 + '@img/sharp-linux-s390x': 0.35.4 + '@img/sharp-linux-x64': 0.35.4 + '@img/sharp-linuxmusl-arm64': 0.35.4 + '@img/sharp-linuxmusl-x64': 0.35.4 + '@img/sharp-webcontainers-wasm32': 0.35.4 + '@img/sharp-win32-arm64': 0.35.4 + '@img/sharp-win32-ia32': 0.35.4 + '@img/sharp-win32-x64': 0.35.4 + '@types/node': 26.4.0 optional: true shebang-command@2.0.0: @@ -11240,6 +10449,10 @@ snapshots: sisteransi@1.0.5: {} + skin-tone@2.0.0: + dependencies: + unicode-emoji-modifier-base: 1.0.0 + slash@3.0.0: {} slice-ansi@3.0.0: @@ -11250,19 +10463,15 @@ snapshots: source-map-js@1.2.1: {} - source-map-support@0.5.13: - dependencies: - buffer-from: 1.1.2 - source-map: 0.6.1 - source-map-support@0.5.21: dependencies: buffer-from: 1.1.2 source-map: 0.6.1 + optional: true source-map@0.6.1: {} - source-map@0.7.4: {} + source-map@0.7.6: {} spdx-correct@3.2.0: dependencies: @@ -11294,10 +10503,7 @@ snapshots: std-env@3.10.0: {} - stop-iteration-iterator@1.1.0: - dependencies: - es-errors: 1.3.0 - internal-slot: 1.1.0 + stdin-discarder@0.3.2: {} stream-browserify@3.0.0: dependencies: @@ -11306,7 +10512,7 @@ snapshots: streamsearch@1.1.0: {} - streamx@2.28.0: + streamx@2.28.1: dependencies: events-universal: 1.0.1 fast-fifo: 1.3.2 @@ -11332,29 +10538,10 @@ snapshots: emoji-regex: 9.2.2 strip-ansi: 7.2.0 - string.prototype.trim@1.2.11: - dependencies: - call-bind: 1.0.9 - call-bound: 1.0.4 - define-data-property: 1.1.4 - define-properties: 1.2.1 - es-abstract: 1.24.2 - es-object-atoms: 1.1.2 - has-property-descriptors: 1.0.2 - safe-regex-test: 1.1.0 - - string.prototype.trimend@1.0.10: + string-width@8.2.2: dependencies: - call-bind: 1.0.9 - call-bound: 1.0.4 - define-properties: 1.2.1 - es-object-atoms: 1.1.2 - - string.prototype.trimstart@1.0.8: - dependencies: - call-bind: 1.0.9 - define-properties: 1.2.1 - es-object-atoms: 1.1.2 + get-east-asian-width: 1.6.0 + strip-ansi: 7.2.0 string_decoder@1.1.1: dependencies: @@ -11370,7 +10557,7 @@ snapshots: strip-ansi@7.2.0: dependencies: - ansi-regex: 6.2.2 + ansi-regex: 6.3.0 strip-bom@3.0.0: {} @@ -11403,7 +10590,7 @@ snapshots: formidable: 3.5.4 methods: 1.1.2 mime: 2.6.0 - qs: 6.15.3 + qs: 6.16.0 transitivePeerDependencies: - supports-color @@ -11430,12 +10617,10 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} - swagger-ui-dist@5.32.8: + swagger-ui-dist@5.32.14: dependencies: '@scarf/scarf': 1.4.0 - symbol-observable@4.0.0: {} - synckit@0.11.13: dependencies: '@pkgr/core': 0.3.6 @@ -11452,12 +10637,12 @@ snapshots: inherits: 2.0.4 readable-stream: 3.6.2 - tar-stream@3.2.0: + tar-stream@3.2.1: dependencies: b4a: 1.8.1 - bare-fs: 4.7.3 + bare-fs: 4.8.1 fast-fifo: 1.3.2 - streamx: 2.28.0 + streamx: 2.28.1 transitivePeerDependencies: - bare-abort-controller - bare-buffer @@ -11465,7 +10650,7 @@ snapshots: teex@1.0.1: dependencies: - streamx: 2.28.0 + streamx: 2.28.1 transitivePeerDependencies: - bare-abort-controller - react-native-b4a @@ -11500,21 +10685,23 @@ snapshots: '@jridgewell/trace-mapping': 0.3.31 jest-worker: 27.5.1 schema-utils: 4.3.3 - terser: 5.48.0 + terser: 5.51.2 webpack: 5.106.2 + optional: true - terser@5.48.0: + terser@5.51.2: dependencies: '@jridgewell/source-map': 0.3.11 - acorn: 8.17.0 + acorn: 8.18.0 commander: 2.20.3 source-map-support: 0.5.21 + optional: true - test-exclude@6.0.0: + test-exclude@7.0.2: dependencies: '@istanbuljs/schema': 0.1.6 - glob: 7.2.3 - minimatch: 3.1.5 + glob: 10.5.0 + minimatch: 10.2.6 text-decoder@1.2.7: dependencies: @@ -11524,14 +10711,10 @@ snapshots: tinyglobby@0.2.17: dependencies: - fdir: 6.5.0(picomatch@4.0.5) - picomatch: 4.0.5 + fdir: 6.5.0(picomatch@4.0.7) + picomatch: 4.0.7 - tmp@0.2.1: - dependencies: - rimraf: 3.0.2 - - tmpl@1.0.5: {} + tmp@0.2.7: {} to-regex-range@5.0.1: dependencies: @@ -11551,12 +10734,12 @@ snapshots: dependencies: typescript: 6.0.3 - ts-jest@29.4.12(@babel/core@7.29.7)(@jest/transform@30.4.1)(@jest/types@30.4.1)(babel-jest@30.4.1(@babel/core@7.29.7))(jest-util@30.4.1)(jest@30.4.2(@types/node@26.1.2)(ts-node@10.9.2(@types/node@26.1.2)(typescript@6.0.3)))(typescript@6.0.3): + ts-jest@29.4.12(@babel/core@7.29.7)(@jest/transform@30.5.0)(@jest/types@30.5.0)(babel-jest@30.5.0(@babel/core@7.29.7))(jest-util@30.5.0)(jest@30.5.0(@types/node@26.4.0)(ts-node@10.9.2(@types/node@26.4.0)(typescript@6.0.3)))(typescript@6.0.3): dependencies: bs-logger: 0.2.6 fast-json-stable-stringify: 2.1.0 handlebars: 4.7.9 - jest: 30.4.2(@types/node@26.1.2)(ts-node@10.9.2(@types/node@26.1.2)(typescript@6.0.3)) + jest: 30.5.0(@types/node@26.4.0)(ts-node@10.9.2(@types/node@26.4.0)(typescript@6.0.3)) json5: 2.2.3 lodash.memoize: 4.1.2 make-error: 1.3.6 @@ -11566,20 +10749,20 @@ snapshots: yargs-parser: 21.1.1 optionalDependencies: '@babel/core': 7.29.7 - '@jest/transform': 30.4.1 - '@jest/types': 30.4.1 - babel-jest: 30.4.1(@babel/core@7.29.7) - jest-util: 30.4.1 + '@jest/transform': 30.5.0 + '@jest/types': 30.5.0 + babel-jest: 30.5.0(@babel/core@7.29.7) + jest-util: 30.5.0 - ts-node@10.9.2(@types/node@26.1.2)(typescript@6.0.3): + ts-node@10.9.2(@types/node@26.4.0)(typescript@6.0.3): dependencies: '@cspotcode/source-map-support': 0.8.1 - '@tsconfig/node10': 1.0.12 + '@tsconfig/node10': 1.0.13 '@tsconfig/node12': 1.0.11 '@tsconfig/node14': 1.0.3 '@tsconfig/node16': 1.0.4 - '@types/node': 26.1.2 - acorn: 8.17.0 + '@types/node': 26.4.0 + acorn: 8.18.0 acorn-walk: 8.3.5 arg: 4.1.3 create-require: 1.1.1 @@ -11594,16 +10777,10 @@ snapshots: tsconfig-paths-webpack-plugin@4.2.0: dependencies: chalk: 4.1.2 - enhanced-resolve: 5.24.1 + enhanced-resolve: 5.24.5 tapable: 2.3.3 tsconfig-paths: 4.2.0 - - tsconfig-paths@3.15.0: - dependencies: - '@types/json5': 0.0.29 - json5: 1.0.2 - minimist: 1.2.8 - strip-bom: 3.0.0 + optional: true tsconfig-paths@4.2.0: dependencies: @@ -11613,14 +10790,14 @@ snapshots: tslib@2.8.1: {} - turbo@2.10.7: + turbo@2.10.12: optionalDependencies: - '@turbo/darwin-64': 2.10.7 - '@turbo/darwin-arm64': 2.10.7 - '@turbo/linux-64': 2.10.7 - '@turbo/linux-arm64': 2.10.7 - '@turbo/windows-64': 2.10.7 - '@turbo/windows-arm64': 2.10.7 + '@turbo/darwin-64': 2.10.12 + '@turbo/darwin-arm64': 2.10.12 + '@turbo/linux-64': 2.10.12 + '@turbo/linux-arm64': 2.10.12 + '@turbo/windows-64': 2.10.12 + '@turbo/windows-arm64': 2.10.12 type-check@0.4.0: dependencies: @@ -11647,47 +10824,12 @@ snapshots: type-is@2.1.0: dependencies: - content-type: 2.0.0 - media-typer: 1.1.0 + content-type: 2.1.0 + media-typer: 1.1.1 mime-types: 3.0.2 - typed-array-buffer@1.0.3: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - is-typed-array: 1.1.15 - - typed-array-byte-length@1.0.3: - dependencies: - call-bind: 1.0.9 - for-each: 0.3.5 - gopd: 1.2.0 - has-proto: 1.2.0 - is-typed-array: 1.1.15 - - typed-array-byte-offset@1.0.4: - dependencies: - available-typed-arrays: 1.0.7 - call-bind: 1.0.9 - for-each: 0.3.5 - gopd: 1.2.0 - has-proto: 1.2.0 - is-typed-array: 1.1.15 - reflect.getprototypeof: 1.0.10 - - typed-array-length@1.0.8: - dependencies: - call-bind: 1.0.9 - for-each: 0.3.5 - gopd: 1.2.0 - is-typed-array: 1.1.15 - possible-typed-array-names: 1.1.0 - reflect.getprototypeof: 1.0.10 - typedarray@0.0.6: {} - typescript@5.9.3: {} - typescript@6.0.3: {} uglify-js@3.19.3: @@ -11699,15 +10841,10 @@ snapshots: uint8array-extras@1.5.0: {} - unbox-primitive@1.1.0: - dependencies: - call-bound: 1.0.4 - has-bigints: 1.1.0 - has-symbols: 1.1.0 - which-boxed-primitive: 1.1.1 - undici-types@8.3.0: {} + unicode-emoji-modifier-base@1.0.0: {} + unique-string@2.0.0: dependencies: crypto-random-string: 2.0.0 @@ -11743,9 +10880,9 @@ snapshots: '@unrs/resolver-binding-win32-ia32-msvc': 1.12.2 '@unrs/resolver-binding-win32-x64-msvc': 1.12.2 - update-browserslist-db@1.2.3(browserslist@4.28.4): + update-browserslist-db@1.3.2(browserslist@4.28.8): dependencies: - browserslist: 4.28.4 + browserslist: 4.28.8 escalade: 3.2.0 picocolors: 1.1.1 @@ -11782,25 +10919,20 @@ snapshots: vary@1.1.2: {} - walker@1.0.8: - dependencies: - makeerror: 1.0.12 - watchpack@2.5.2: dependencies: graceful-fs: 4.2.11 - - wcwidth@1.0.1: - dependencies: - defaults: 1.0.4 + optional: true web-streams-polyfill@3.3.3: {} webidl-conversions@3.0.1: {} - webpack-node-externals@3.0.0: {} + webpack-node-externals@3.0.0: + optional: true - webpack-sources@3.5.0: {} + webpack-sources@3.5.1: + optional: true webpack@5.106.2: dependencies: @@ -11810,12 +10942,12 @@ snapshots: '@webassemblyjs/ast': 1.14.1 '@webassemblyjs/wasm-edit': 1.14.1 '@webassemblyjs/wasm-parser': 1.14.1 - acorn: 8.17.0 - acorn-import-phases: 1.0.4(acorn@8.17.0) - browserslist: 4.28.4 + acorn: 8.18.0 + acorn-import-phases: 1.0.4(acorn@8.18.0) + browserslist: 4.28.8 chrome-trace-event: 1.0.4 - enhanced-resolve: 5.24.1 - es-module-lexer: 2.3.0 + enhanced-resolve: 5.24.5 + es-module-lexer: 2.3.2 eslint-scope: 5.1.1 events: 3.3.0 glob-to-regexp: 0.4.1 @@ -11827,7 +10959,7 @@ snapshots: tapable: 2.3.3 terser-webpack-plugin: 5.6.1(webpack@5.106.2) watchpack: 2.5.2 - webpack-sources: 3.5.0 + webpack-sources: 3.5.1 transitivePeerDependencies: - '@minify-html/node' - '@swc/core' @@ -11841,53 +10973,13 @@ snapshots: - lightningcss - postcss - uglify-js + optional: true whatwg-url@5.0.0: dependencies: tr46: 0.0.3 webidl-conversions: 3.0.1 - which-boxed-primitive@1.1.1: - dependencies: - is-bigint: 1.1.0 - is-boolean-object: 1.2.2 - is-number-object: 1.1.1 - is-string: 1.1.1 - is-symbol: 1.1.1 - - which-builtin-type@1.2.1: - dependencies: - call-bound: 1.0.4 - function.prototype.name: 1.2.0 - has-tostringtag: 1.0.2 - is-async-function: 2.1.1 - is-date-object: 1.1.0 - is-finalizationregistry: 1.1.1 - is-generator-function: 1.1.2 - is-regex: 1.2.1 - is-weakref: 1.1.1 - isarray: 2.0.5 - which-boxed-primitive: 1.1.1 - which-collection: 1.0.2 - which-typed-array: 1.1.22 - - which-collection@1.0.2: - dependencies: - is-map: 2.0.3 - is-set: 2.0.3 - is-weakmap: 2.0.2 - is-weakset: 2.0.4 - - which-typed-array@1.1.22: - dependencies: - available-typed-arrays: 1.0.7 - call-bind: 1.0.9 - call-bound: 1.0.4 - for-each: 0.3.5 - get-proto: 1.0.1 - gopd: 1.2.0 - has-tostringtag: 1.0.2 - which@2.0.2: dependencies: isexe: 2.0.0 @@ -11900,12 +10992,6 @@ snapshots: wordwrap@1.0.0: {} - wrap-ansi@6.2.0: - dependencies: - ansi-styles: 4.3.0 - string-width: 4.2.3 - strip-ansi: 6.0.1 - wrap-ansi@7.0.0: dependencies: ansi-styles: 4.3.0 @@ -11947,7 +11033,7 @@ snapshots: yocto-queue@0.1.0: {} - yoctocolors-cjs@2.1.3: {} + yoctocolors@2.2.0: {} zeptomatch@2.1.0: dependencies: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 3ca5057..db06a33 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -2,20 +2,25 @@ packages: - 'apps/*' - 'packages/*' overrides: - # googleapis 173 pulls google-auth-library ^10.2.0 (10.9.0) while its own - # googleapis-common pins 10.5.0 exactly — two copies make the OAuth2Client - # types incompatible. Dedupe to a single (latest) version. + # googleapis 176 and its googleapis-common both pin google-auth-library 10.5.0, but other + # dependents resolve 10.9.0 — without this override pnpm installs both copies and the two + # OAuth2Client types stop being assignable. Dedupe to a single (latest) version. google-auth-library: 10.9.0 # Security: force patched transitive versions (advisories on the ranges Next # pulled). sharp < 0.35.0 = libvips CVEs (image optimization, production); # postcss <= 8.5.17 = arbitrary file read / path traversal (build-time CSS). sharp: ^0.35.3 postcss: ^8.5.23 - # Build/dev-tooling transitives (Prisma dev, ajv, glob). Patched within-major - # (no breaking bump): fast-uri host confusion, brace-expansion 5.x DoS, - # @hono/node-server middleware bypass / path traversal, valibot flatten() throw. - fast-uri@3: 3.1.4 - brace-expansion: 5.0.8 + # Build/dev-tooling transitives (Prisma dev, ajv, glob, eslint). None sit in the running + # server's request path; they are pinned so `pnpm audit` stays clean and a vulnerable copy + # cannot sneak into a build. Patched within-major only (no breaking bump). + # Keep these at or above the advisory's patched version — a stale pin is worse than none, + # because it silently holds the vulnerable copy in place. + fast-uri@3: 3.1.5 + brace-expansion: 5.0.9 + # cross-spawn <7.0.5 ReDoS; tmp <0.2.6 path traversal via unsanitized prefix. + cross-spawn: ^7.0.6 + tmp: ^0.2.6 '@hono/node-server': ^1.19.17 valibot: ^1.4.2 # js-yaml 5.x flow-collection DoS (only the 5.x copy under @nestjs/swagger, dev-only); @@ -23,6 +28,8 @@ overrides: js-yaml@5: 5.2.2 find-my-way: ^9.6.1 allowBuilds: + # Jest's optional native file watcher — never built here and the suites do not need it. + '@parcel/watcher': false '@prisma/engines': true '@scarf/scarf': false '@swc/core': true diff --git a/postman/dropto-api.postman_collection.json b/postman/dropto-api.postman_collection.json new file mode 100644 index 0000000..ccfd669 --- /dev/null +++ b/postman/dropto-api.postman_collection.json @@ -0,0 +1,1289 @@ +{ + "info": { + "name": "DropTo API \u2014 runtime regression", + "description": "End-to-end regression run against a live DropTo API. Mutating requests stay inside the first authorized Drive root and clean up after themselves (the uploaded file and created folder are deleted at the end).\n\nRun:\n postman collection run postman/dropto-api.postman_collection.json -e postman/local.postman_environment.json --env-var token=\n\nMust be run from the repo root so the upload fixture path resolves.\n\nNote: the Auth folder spends the /auth/login rate-limit budget (5 attempts / 60 s, 900 s block). Re-running inside 15 minutes answers 429 there; those three assertions accept both codes on purpose.\n\nWARNING: this collection MUTATES the storage it points at. It creates a folder and uploads a file into the FIRST authorized root, then deletes both at the end. Point it at a local API (the default baseUrl) \u2014 against production it writes into the Drive owner's real folder, and a run that fails midway leaves a `__runtime-test-` folder behind.", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" + }, + "variable": [ + { + "key": "host", + "value": "http://127.0.0.1:4000" + }, + { + "key": "baseUrl", + "value": "http://127.0.0.1:4000/api/v1" + }, + { + "key": "token", + "value": "" + }, + { + "key": "rootId", + "value": "" + }, + { + "key": "folderId", + "value": "" + }, + { + "key": "fileId", + "value": "" + } + ], + "item": [ + { + "name": "Health", + "item": [ + { + "name": "GET /health (public, off the global prefix)", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('status is 200', () => pm.response.to.have.status(200));", + "pm.test('reports ok with postgres up', () => {", + " const b = pm.response.json();", + " pm.expect(b.status).to.eql('ok');", + " pm.expect(b.info.postgres.status).to.eql('up');", + "});", + "pm.test('responds under 1000ms', () => pm.expect(pm.response.responseTime).to.be.below(1000));" + ] + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": "{{host}}/health" + } + } + ] + }, + { + "name": "Auth", + "item": [ + { + "name": "GET /auth/me without a cookie -> 401", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('status is 401', () => pm.response.to.have.status(401));", + "pm.test('error body has the { status, message } shape', function () {", + " const b = pm.response.json();", + " pm.expect(b).to.have.property('status');", + " pm.expect(b).to.have.property('message');", + "});", + "pm.test('responds under 3000ms', () => pm.expect(pm.response.responseTime).to.be.below(3000));" + ] + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": "{{baseUrl}}/auth/me" + } + }, + { + "name": "GET /auth/me with a malformed token -> 401", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('status is 401', () => pm.response.to.have.status(401));", + "pm.test('error body has the { status, message } shape', function () {", + " const b = pm.response.json();", + " pm.expect(b).to.have.property('status');", + " pm.expect(b).to.have.property('message');", + "});", + "pm.test('responds under 3000ms', () => pm.expect(pm.response.responseTime).to.be.below(3000));" + ] + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Cookie", + "value": "accessToken=not-a-real-jwt" + } + ], + "url": "{{baseUrl}}/auth/me" + } + }, + { + "name": "GET /auth/me authenticated -> 200", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('status is 200', () => pm.response.to.have.status(200));", + "pm.test('returns the operator username', () => pm.expect(pm.response.json()).to.have.property('username'));", + "pm.test('responds under 3000ms', () => pm.expect(pm.response.responseTime).to.be.below(3000));" + ] + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Cookie", + "value": "accessToken={{token}}" + } + ], + "url": "{{baseUrl}}/auth/me" + } + }, + { + "name": "POST /auth/login with an empty body -> 400 (or 429 if the limiter is armed)", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "// /auth/login is limited to 5 attempts / 60 s with a 900 s block, and the guard runs before the", + "// validation pipe \u2014 so a re-run inside 15 minutes legitimately answers 429 instead.", + "pm.test('is rejected (400, or 429 once the limiter is armed)', function () {", + " pm.expect([400, 429]).to.include(pm.response.code);", + "});", + "pm.test('error body has the { status, message } shape', function () {", + " const b = pm.response.json();", + " pm.expect(b).to.have.property('status');", + " pm.expect(b).to.have.property('message');", + "});", + "pm.test('responds under 3000ms', () => pm.expect(pm.response.responseTime).to.be.below(3000));" + ] + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": "{{baseUrl}}/auth/login", + "body": { + "mode": "raw", + "raw": "{}", + "options": { + "raw": { + "language": "json" + } + } + } + } + }, + { + "name": "POST /auth/login with an unknown field -> 400 (or 429 if the limiter is armed)", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "// /auth/login is limited to 5 attempts / 60 s with a 900 s block, and the guard runs before the", + "// validation pipe \u2014 so a re-run inside 15 minutes legitimately answers 429 instead.", + "pm.test('is rejected (400 forbidNonWhitelisted, or 429)', function () {", + " pm.expect([400, 429]).to.include(pm.response.code);", + "});", + "pm.test('error body has the { status, message } shape', function () {", + " const b = pm.response.json();", + " pm.expect(b).to.have.property('status');", + " pm.expect(b).to.have.property('message');", + "});", + "pm.test('responds under 3000ms', () => pm.expect(pm.response.responseTime).to.be.below(3000));" + ] + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": "{{baseUrl}}/auth/login", + "body": { + "mode": "raw", + "raw": "{\"username\": \"x\", \"password\": \"y\", \"role\": \"admin\"}", + "options": { + "raw": { + "language": "json" + } + } + } + } + }, + { + "name": "POST /auth/login with wrong credentials -> 401 (or 429 if the limiter is armed)", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "// 5 attempts / 60 s with a 900 s block, so a re-run inside 15 minutes legitimately gets 429.", + "// Either way the point holds: bad credentials never yield a session.", + "pm.test('is rejected (401, or 429 once the limiter is armed)', function () {", + " pm.expect([401, 429]).to.include(pm.response.code);", + "});", + "pm.test('sets no auth cookie on a failed login', function () {", + " const sc = pm.response.headers.get('set-cookie') || '';", + " pm.expect(sc).to.not.include('accessToken=ey');", + "});", + "pm.test('responds under 3000ms', () => pm.expect(pm.response.responseTime).to.be.below(3000));" + ] + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": "{{baseUrl}}/auth/login", + "body": { + "mode": "raw", + "raw": "{\"username\": \"definitely-not-the-operator\", \"password\": \"definitely-not-the-password\"}", + "options": { + "raw": { + "language": "json" + } + } + } + } + }, + { + "name": "POST /auth/refresh without a refresh cookie -> 401", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('status is 401', () => pm.response.to.have.status(401));", + "pm.test('error body has the { status, message } shape', function () {", + " const b = pm.response.json();", + " pm.expect(b).to.have.property('status');", + " pm.expect(b).to.have.property('message');", + "});", + "pm.test('responds under 3000ms', () => pm.expect(pm.response.responseTime).to.be.below(3000));" + ] + } + } + ], + "request": { + "method": "POST", + "header": [], + "url": "{{baseUrl}}/auth/refresh" + } + } + ] + }, + { + "name": "Google Auth", + "item": [ + { + "name": "GET /google-auth/status -> 200", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('status is 200', () => pm.response.to.have.status(200));", + "pm.test('reports a connected account', () => pm.expect(pm.response.json()).to.have.property('connected'));", + "pm.test('responds under 3000ms', () => pm.expect(pm.response.responseTime).to.be.below(3000));" + ] + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Cookie", + "value": "accessToken={{token}}" + } + ], + "url": "{{baseUrl}}/google-auth/status" + } + }, + { + "name": "GET /google-auth/picker-token -> 200", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('status is 200', () => pm.response.to.have.status(200));", + "pm.test('issues a short-lived access token', () => pm.expect(pm.response.json().accessToken).to.be.a('string').and.not.empty);", + "pm.test('responds under 5000ms', () => pm.expect(pm.response.responseTime).to.be.below(5000));" + ] + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Cookie", + "value": "accessToken={{token}}" + } + ], + "url": "{{baseUrl}}/google-auth/picker-token" + } + }, + { + "name": "POST /google-auth/folders without the owner cookie -> 403", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('status is 403', () => pm.response.to.have.status(403));", + "pm.test('error body has the { status, message } shape', function () {", + " const b = pm.response.json();", + " pm.expect(b).to.have.property('status');", + " pm.expect(b).to.have.property('message');", + "});", + "pm.test('responds under 3000ms', () => pm.expect(pm.response.responseTime).to.be.below(3000));" + ] + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Cookie", + "value": "accessToken={{token}}" + } + ], + "url": "{{baseUrl}}/google-auth/folders", + "body": { + "mode": "raw", + "raw": "{\"folders\": [{\"folderId\": \"injected\", \"name\": \"injected\"}]}", + "options": { + "raw": { + "language": "json" + } + } + } + } + }, + { + "name": "DELETE /google-auth/folders/:id without the owner cookie -> 403", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "// A literal id: the owner guard rejects before any lookup, so this must not depend on run order.", + "pm.test('status is 403', () => pm.response.to.have.status(403));", + "pm.test('error body has the { status, message } shape', function () {", + " const b = pm.response.json();", + " pm.expect(b).to.have.property('status');", + " pm.expect(b).to.have.property('message');", + "});", + "pm.test('responds under 3000ms', () => pm.expect(pm.response.responseTime).to.be.below(3000));" + ] + } + } + ], + "request": { + "method": "DELETE", + "header": [ + { + "key": "Cookie", + "value": "accessToken={{token}}" + } + ], + "url": "{{baseUrl}}/google-auth/folders/not-a-real-folder-id" + } + }, + { + "name": "DELETE /google-auth/account without the owner cookie -> 403", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('status is 403', () => pm.response.to.have.status(403));", + "pm.test('error body has the { status, message } shape', function () {", + " const b = pm.response.json();", + " pm.expect(b).to.have.property('status');", + " pm.expect(b).to.have.property('message');", + "});", + "pm.test('responds under 3000ms', () => pm.expect(pm.response.responseTime).to.be.below(3000));" + ] + } + } + ], + "request": { + "method": "DELETE", + "header": [ + { + "key": "Cookie", + "value": "accessToken={{token}}" + } + ], + "url": "{{baseUrl}}/google-auth/account" + } + } + ] + }, + { + "name": "Storage (generic)", + "item": [ + { + "name": "GET /storage/status -> 200", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('status is 200', () => pm.response.to.have.status(200));", + "pm.test('reports every configured backend', function () {", + " const b = pm.response.json();", + " pm.expect(b).to.be.an('array').with.length.of.at.least(1);", + " pm.expect(b.map(s => s.backend)).to.include('drive');", + "});", + "pm.test('responds under 5000ms', () => pm.expect(pm.response.responseTime).to.be.below(5000));" + ] + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Cookie", + "value": "accessToken={{token}}" + } + ], + "url": "{{baseUrl}}/storage/status" + } + }, + { + "name": "GET /storage/nope/folders (unknown backend) -> 404", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('status is 404', () => pm.response.to.have.status(404));", + "pm.test('error body has the { status, message } shape', function () {", + " const b = pm.response.json();", + " pm.expect(b).to.have.property('status');", + " pm.expect(b).to.have.property('message');", + "});", + "pm.test('responds under 3000ms', () => pm.expect(pm.response.responseTime).to.be.below(3000));" + ] + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Cookie", + "value": "accessToken={{token}}" + } + ], + "url": "{{baseUrl}}/storage/nope/folders" + } + }, + { + "name": "GET /storage/s3/folders while S3 is disabled -> 404", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('status is 404', () => pm.response.to.have.status(404));", + "pm.test('error body has the { status, message } shape', function () {", + " const b = pm.response.json();", + " pm.expect(b).to.have.property('status');", + " pm.expect(b).to.have.property('message');", + "});", + "pm.test('responds under 3000ms', () => pm.expect(pm.response.responseTime).to.be.below(3000));" + ] + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Cookie", + "value": "accessToken={{token}}" + } + ], + "url": "{{baseUrl}}/storage/s3/folders" + } + }, + { + "name": "GET /storage/drive/folders without a cookie -> 401", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('status is 401', () => pm.response.to.have.status(401));", + "pm.test('error body has the { status, message } shape', function () {", + " const b = pm.response.json();", + " pm.expect(b).to.have.property('status');", + " pm.expect(b).to.have.property('message');", + "});", + "pm.test('responds under 3000ms', () => pm.expect(pm.response.responseTime).to.be.below(3000));" + ] + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": "{{baseUrl}}/storage/drive/folders" + } + } + ] + }, + { + "name": "Drive (browse)", + "item": [ + { + "name": "GET /storage/drive/folders -> 200 (captures rootId)", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('status is 200', () => pm.response.to.have.status(200));", + "pm.test('returns the authorized roots', function () {", + " const b = pm.response.json();", + " pm.expect(b).to.be.an('array').that.is.not.empty;", + " pm.collectionVariables.set('rootId', b[0].folderId);", + "});", + "pm.test('responds under 5000ms', () => pm.expect(pm.response.responseTime).to.be.below(5000));" + ] + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Cookie", + "value": "accessToken={{token}}" + } + ], + "url": "{{baseUrl}}/storage/drive/folders" + } + }, + { + "name": "GET /storage/drive/folders/:root/contents -> 200", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('status is 200', () => pm.response.to.have.status(200));", + "pm.test('returns a page of entries', function () {", + " const b = pm.response.json();", + " pm.expect(b.entries).to.be.an('array');", + " pm.expect(b).to.have.property('nextPageToken');", + "});", + "pm.test('responds under 5000ms', () => pm.expect(pm.response.responseTime).to.be.below(5000));" + ] + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Cookie", + "value": "accessToken={{token}}" + } + ], + "url": "{{baseUrl}}/storage/drive/folders/{{rootId}}/contents" + } + }, + { + "name": "GET contents of an id outside the authorized tree -> 403/404", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('is refused (403 or 404), never 200', () => pm.expect([403,404]).to.include(pm.response.code));", + "pm.test('error body has the { status, message } shape', function () {", + " const b = pm.response.json();", + " pm.expect(b).to.have.property('status');", + " pm.expect(b).to.have.property('message');", + "});", + "pm.test('responds under 5000ms', () => pm.expect(pm.response.responseTime).to.be.below(5000));" + ] + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Cookie", + "value": "accessToken={{token}}" + } + ], + "url": "{{baseUrl}}/storage/drive/folders/1AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/contents" + } + }, + { + "name": "GET /storage/drive/names over the 50-id cap -> 400", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('status is 400', () => pm.response.to.have.status(400));", + "pm.test('error body has the { status, message } shape', function () {", + " const b = pm.response.json();", + " pm.expect(b).to.have.property('status');", + " pm.expect(b).to.have.property('message');", + "});", + "pm.test('responds under 3000ms', () => pm.expect(pm.response.responseTime).to.be.below(3000));" + ] + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Cookie", + "value": "accessToken={{token}}" + } + ], + "url": "{{baseUrl}}/storage/drive/names?ids=id-0,id-1,id-2,id-3,id-4,id-5,id-6,id-7,id-8,id-9,id-10,id-11,id-12,id-13,id-14,id-15,id-16,id-17,id-18,id-19,id-20,id-21,id-22,id-23,id-24,id-25,id-26,id-27,id-28,id-29,id-30,id-31,id-32,id-33,id-34,id-35,id-36,id-37,id-38,id-39,id-40,id-41,id-42,id-43,id-44,id-45,id-46,id-47,id-48,id-49,id-50" + } + }, + { + "name": "GET /storage/drive/names for the root -> 200", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('status is 200', () => pm.response.to.have.status(200));", + "pm.test('resolves the id to a name', function () {", + " const b = pm.response.json();", + " pm.expect(b).to.be.an('array').with.lengthOf(1);", + " pm.expect(b[0]).to.have.property('name');", + "});", + "pm.test('responds under 5000ms', () => pm.expect(pm.response.responseTime).to.be.below(5000));" + ] + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Cookie", + "value": "accessToken={{token}}" + } + ], + "url": "{{baseUrl}}/storage/drive/names?ids={{rootId}}" + } + } + ] + }, + { + "name": "Drive (mutations, inside the authorized root)", + "item": [ + { + "name": "POST subfolder with a slash in the name -> 400", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('status is 400', () => pm.response.to.have.status(400));", + "pm.test('error body has the { status, message } shape', function () {", + " const b = pm.response.json();", + " pm.expect(b).to.have.property('status');", + " pm.expect(b).to.have.property('message');", + "});", + "pm.test('responds under 3000ms', () => pm.expect(pm.response.responseTime).to.be.below(3000));" + ] + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Cookie", + "value": "accessToken={{token}}" + } + ], + "url": "{{baseUrl}}/storage/drive/folders/{{rootId}}/subfolder", + "body": { + "mode": "raw", + "raw": "{\"name\": \"bad/name\"}", + "options": { + "raw": { + "language": "json" + } + } + } + } + }, + { + "name": "POST subfolder -> 201 (captures folderId)", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('status is 201', () => pm.response.to.have.status(201));", + "pm.test('creates a folder', function () {", + " const b = pm.response.json();", + " pm.expect(b.isFolder).to.be.true;", + " pm.collectionVariables.set('folderId', b.id);", + "});", + "pm.test('responds under 5000ms', () => pm.expect(pm.response.responseTime).to.be.below(5000));" + ] + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Cookie", + "value": "accessToken={{token}}" + } + ], + "url": "{{baseUrl}}/storage/drive/folders/{{rootId}}/subfolder", + "body": { + "mode": "raw", + "raw": "{\"name\": \"__runtime-test-{{$timestamp}}\"}", + "options": { + "raw": { + "language": "json" + } + } + } + } + }, + { + "name": "POST upload a file -> 201 (captures fileId)", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('status is 201', () => pm.response.to.have.status(201));", + "pm.test('stores the file and reports its size', function () {", + " const b = pm.response.json();", + " pm.expect(b.fileName).to.eql('sample.txt');", + " pm.expect(b.size).to.be.above(0);", + " pm.collectionVariables.set('fileId', b.fileId);", + "});", + "pm.test('responds under 10000ms', () => pm.expect(pm.response.responseTime).to.be.below(10000));" + ] + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Cookie", + "value": "accessToken={{token}}" + } + ], + "url": "{{baseUrl}}/storage/drive/folders/{{folderId}}/upload", + "body": { + "mode": "formdata", + "formdata": [ + { + "key": "file", + "type": "file", + "src": "postman/fixtures/sample.txt" + } + ] + } + } + }, + { + "name": "POST upload with no file attached -> 400", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('status is 400', () => pm.response.to.have.status(400));", + "pm.test('error body has the { status, message } shape', function () {", + " const b = pm.response.json();", + " pm.expect(b).to.have.property('status');", + " pm.expect(b).to.have.property('message');", + "});", + "pm.test('responds under 3000ms', () => pm.expect(pm.response.responseTime).to.be.below(3000));" + ] + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Cookie", + "value": "accessToken={{token}}" + } + ], + "url": "{{baseUrl}}/storage/drive/folders/{{folderId}}/upload" + } + }, + { + "name": "GET download the uploaded file -> 200", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('status is 200', () => pm.response.to.have.status(200));", + "pm.test('streams it back with a filename', function () {", + " pm.expect(pm.response.headers.get('content-disposition')).to.include('sample.txt');", + "});", + "pm.test('body round-trips intact', () => pm.expect(pm.response.text()).to.include('DropTo runtime smoke fixture'));", + "pm.test('responds under 10000ms', () => pm.expect(pm.response.responseTime).to.be.below(10000));" + ] + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Cookie", + "value": "accessToken={{token}}" + } + ], + "url": "{{baseUrl}}/storage/drive/files/{{fileId}}/download" + } + }, + { + "name": "PATCH rename the file -> 200", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('status is 200', () => pm.response.to.have.status(200));", + "pm.test('the new name is returned', () => pm.expect(pm.response.json().name).to.eql('renamed.txt'));", + "pm.test('responds under 5000ms', () => pm.expect(pm.response.responseTime).to.be.below(5000));" + ] + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Cookie", + "value": "accessToken={{token}}" + } + ], + "url": "{{baseUrl}}/storage/drive/files/{{fileId}}/rename", + "body": { + "mode": "raw", + "raw": "{\"name\": \"renamed.txt\"}", + "options": { + "raw": { + "language": "json" + } + } + } + } + }, + { + "name": "PATCH rename an authorized root -> 409", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('status is 409', () => pm.response.to.have.status(409));", + "pm.test('error body has the { status, message } shape', function () {", + " const b = pm.response.json();", + " pm.expect(b).to.have.property('status');", + " pm.expect(b).to.have.property('message');", + "});", + "pm.test('responds under 3000ms', () => pm.expect(pm.response.responseTime).to.be.below(3000));" + ] + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Cookie", + "value": "accessToken={{token}}" + } + ], + "url": "{{baseUrl}}/storage/drive/files/{{rootId}}/rename", + "body": { + "mode": "raw", + "raw": "{\"name\": \"hijacked\"}", + "options": { + "raw": { + "language": "json" + } + } + } + } + }, + { + "name": "PATCH move an item into itself -> 400", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('status is 400', () => pm.response.to.have.status(400));", + "pm.test('error body has the { status, message } shape', function () {", + " const b = pm.response.json();", + " pm.expect(b).to.have.property('status');", + " pm.expect(b).to.have.property('message');", + "});", + "pm.test('responds under 3000ms', () => pm.expect(pm.response.responseTime).to.be.below(3000));" + ] + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Cookie", + "value": "accessToken={{token}}" + } + ], + "url": "{{baseUrl}}/storage/drive/files/{{fileId}}/move", + "body": { + "mode": "raw", + "raw": "{\"targetFolderId\": \"{{fileId}}\"}", + "options": { + "raw": { + "language": "json" + } + } + } + } + }, + { + "name": "PATCH move the file up into the root -> 200", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('status is 200', () => pm.response.to.have.status(200));", + "pm.test('responds under 5000ms', () => pm.expect(pm.response.responseTime).to.be.below(5000));" + ] + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Cookie", + "value": "accessToken={{token}}" + } + ], + "url": "{{baseUrl}}/storage/drive/files/{{fileId}}/move", + "body": { + "mode": "raw", + "raw": "{\"targetFolderId\": \"{{rootId}}\"}", + "options": { + "raw": { + "language": "json" + } + } + } + } + }, + { + "name": "GET download the folder as a ZIP -> 200", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('status is 200', () => pm.response.to.have.status(200));", + "pm.test('is a zip stream', function () {", + " pm.expect(pm.response.headers.get('content-type')).to.include('zip');", + " pm.expect(pm.response.headers.get('content-disposition')).to.include('.zip');", + "});", + "pm.test('responds under 15000ms', () => pm.expect(pm.response.responseTime).to.be.below(15000));" + ] + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Cookie", + "value": "accessToken={{token}}" + } + ], + "url": "{{baseUrl}}/storage/drive/folders/{{folderId}}/download" + } + }, + { + "name": "POST upload-status for a non-Google URL -> 400 (SSRF guard)", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('status is 400', () => pm.response.to.have.status(400));", + "pm.test('error body has the { status, message } shape', function () {", + " const b = pm.response.json();", + " pm.expect(b).to.have.property('status');", + " pm.expect(b).to.have.property('message');", + "});", + "pm.test('responds under 2000ms', () => pm.expect(pm.response.responseTime).to.be.below(2000));" + ] + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Cookie", + "value": "accessToken={{token}}" + } + ], + "url": "{{baseUrl}}/storage/drive/upload-status", + "body": { + "mode": "raw", + "raw": "{\"uploadUrl\": \"http://169.254.169.254/latest/meta-data/\", \"size\": 100}", + "options": { + "raw": { + "language": "json" + } + } + } + } + }, + { + "name": "POST upload-session over the size cap -> 400", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('status is 400', () => pm.response.to.have.status(400));", + "pm.test('error body has the { status, message } shape', function () {", + " const b = pm.response.json();", + " pm.expect(b).to.have.property('status');", + " pm.expect(b).to.have.property('message');", + "});", + "pm.test('responds under 3000ms', () => pm.expect(pm.response.responseTime).to.be.below(3000));" + ] + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Cookie", + "value": "accessToken={{token}}" + } + ], + "url": "{{baseUrl}}/storage/drive/folders/{{rootId}}/upload-session", + "body": { + "mode": "raw", + "raw": "{\"name\": \"huge.bin\", \"mimeType\": \"application/octet-stream\", \"size\": 99999999999}", + "options": { + "raw": { + "language": "json" + } + } + } + } + }, + { + "name": "DELETE the uploaded file -> 204 (cleanup)", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('status is 204', () => pm.response.to.have.status(204));", + "pm.test('responds under 5000ms', () => pm.expect(pm.response.responseTime).to.be.below(5000));" + ] + } + } + ], + "request": { + "method": "DELETE", + "header": [ + { + "key": "Cookie", + "value": "accessToken={{token}}" + } + ], + "url": "{{baseUrl}}/storage/drive/files/{{fileId}}" + } + }, + { + "name": "DELETE the created folder -> 204 (cleanup)", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('status is 204', () => pm.response.to.have.status(204));", + "pm.test('responds under 5000ms', () => pm.expect(pm.response.responseTime).to.be.below(5000));" + ] + } + } + ], + "request": { + "method": "DELETE", + "header": [ + { + "key": "Cookie", + "value": "accessToken={{token}}" + } + ], + "url": "{{baseUrl}}/storage/drive/files/{{folderId}}" + } + }, + { + "name": "DELETE an authorized root -> 409", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('status is 409', () => pm.response.to.have.status(409));", + "pm.test('error body has the { status, message } shape', function () {", + " const b = pm.response.json();", + " pm.expect(b).to.have.property('status');", + " pm.expect(b).to.have.property('message');", + "});", + "pm.test('responds under 5000ms', () => pm.expect(pm.response.responseTime).to.be.below(5000));" + ] + } + } + ], + "request": { + "method": "DELETE", + "header": [ + { + "key": "Cookie", + "value": "accessToken={{token}}" + } + ], + "url": "{{baseUrl}}/storage/drive/files/{{rootId}}" + } + } + ] + } + ] +} diff --git a/postman/fixtures/sample.txt b/postman/fixtures/sample.txt new file mode 100644 index 0000000..137237e --- /dev/null +++ b/postman/fixtures/sample.txt @@ -0,0 +1,2 @@ +DropTo runtime smoke fixture. +Uploaded by the Postman regression collection. diff --git a/postman/local.postman_environment.json b/postman/local.postman_environment.json new file mode 100644 index 0000000..2bcb9fe --- /dev/null +++ b/postman/local.postman_environment.json @@ -0,0 +1,21 @@ +{ + "name": "DropTo local", + "values": [ + { + "key": "host", + "value": "http://127.0.0.1:4000", + "enabled": true + }, + { + "key": "baseUrl", + "value": "http://127.0.0.1:4000/api/v1", + "enabled": true + }, + { + "key": "token", + "value": "", + "enabled": true + } + ], + "_postman_variable_scope": "environment" +} diff --git a/turbo.json b/turbo.json index fd36514..9a811ba 100644 --- a/turbo.json +++ b/turbo.json @@ -7,6 +7,9 @@ "inputs": ["$TURBO_DEFAULT$", ".env*"], "outputs": ["dist/**", ".next/**", "!.next/cache/**"] }, + "@dropto/types#build": { + "outputs": [] + }, "dev": { "cache": false, "persistent": true