diff --git a/README.md b/README.md index ef0b4c2..4c07130 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@

- GDSC VIT + GDG VIT

Envoy - Secure Environment Variable Management

A secure platform for managing and sharing environment variables across development teams

@@ -10,88 +10,73 @@ [![Join Us](https://img.shields.io/badge/Join%20Us-Developer%20Student%20Clubs-red)](https://dsc.community.dev/vellore-institute-of-technology/) [![Discord Chat](https://img.shields.io/discord/760928671698649098.svg)](https://discord.gg/498KVdSKWR) -[![DOCS](https://img.shields.io/badge/Documentation-see%20docs-green?style=flat-square&logo=appveyor)](https://github.com/GDGVIT/env-sharing) +[![DOCS](https://img.shields.io/badge/Documentation-see%20docs-green?style=flat-square&logo=appveyor)](https://github.com/GDGVIT/envoy) [![UI ](https://img.shields.io/badge/User%20Interface-Link%20to%20UI-orange?style=flat-square&logo=appveyor)](https://env-sharing-platform.vercel.app) ## Overview The Environment Sharing Platform is a secure web application that enables development teams to safely manage, store, and share environment variables without exposing sensitive credentials through insecure channels like Slack or email. Built with Next.js and MongoDB, it provides an encrypted, centralized vault for all your environment variables. +### How it's organized + +``` +Organization owners, admins and members +└ Team each member is an Editor or a Viewer + └ Project one app or service + └ Environment development, staging, production, … (each with its own variables) +``` + +| Role | Can do | +| --- | --- | +| **Org owner** | Everything, plus delete the org, transfer ownership, require two-factor, reset a member's 2FA | +| **Org admin** | Create teams, invite people, change roles, edit every project, view the audit log | +| **Org member** | See only the teams they're on | +| **Team editor** | Read and change variables, create projects and environments in that team | +| **Team viewer** | Read variables only | + ## Features -- [x] **Secure Environment Variable Storage** - Encrypted storage of API keys, database credentials, and other sensitive configuration data -- [x] **Team Collaboration** - Create workspaces and groups to organize and share environment variables with team members -- [x] **Role-based Access Control** - Owner and member roles with appropriate permissions for workspace and group management -- [x] **Authentication System** - Secure user registration and login with NextAuth.js integration -- [x] **Workspace Management** - Create, edit, and delete workspaces to organize your projects -- [x] **Group Management** - Organize environment variables into groups within workspaces -- [x] **Member Management** - Add and remove team members from groups and workspaces -- [x] **Sharing Tokens** - Generate secure tokens for sharing environment variables -- [x] **Dark/Light Theme** - Toggle between dark and light themes for better user experience -- [x] **Responsive Design** - Fully responsive design that works on desktop and mobile devices +- [x] **Organizations, teams and projects** - Org owners and admins create teams and decide who can edit and who can only read +- [x] **Multiple environments per project** - development, staging and production out of the box; add your own +- [x] **Sign in with Google** - Accounts are created with Google; people can add a password in Account settings for email sign-in +- [x] **Passkeys and authenticator apps** - Optional second step at sign-in (Touch ID, Face ID, Windows Hello, a password manager, or 6-digit codes), with one-time backup codes. Owners can require it for their organization +- [x] **Values stay hidden until you confirm** - People with two-factor confirm with a passkey or code before secret values are sent to the browser; the unlock lasts 10 minutes and lives on the server +- [x] **Restricted projects** - Limit a project to selected team members; move projects between teams +- [x] **Invitations** - 10-character invite codes (also shareable as a link) that expire and can be limited to a number of uses. Envoy doesn't send email. +- [x] **Audit log** - Owners and admins see every change to members, teams, projects and variables (key names only, never values) +- [x] **Personal API tokens** - Fetch variables from the CLI or CI; tokens follow the user's current access +- [x] **Encrypted at rest** - AES-256-GCM, decrypted only for people with access +- [x] **Safe concurrent editing** - Saves are versioned, so two editors can't silently overwrite each other +- [x] **Temporary links** - Share a .env once without an account; links expire within 24 hours and are deleted. Environments are never public. +- [x] **Monochrome UI** - shadcn/ui, dark and light themes, ⌘K command menu, keyboard shortcuts, responsive down to mobile - [x] **Rate Limiting** - Built-in rate limiting to prevent abuse and ensure system stability -- [ ] **NodeJS and Python SDK** - Fetch enviornment variables directly from the terminal -- [ ] **Basic Groups** - Create basic groups with limited features +- [ ] **NodeJS and Python SDK** - Fetch environment variables directly from the terminal
## Tech Stack ### Frontend -- **Next.js 15** - React framework for production -- **React 18** - JavaScript library for building user interfaces -- **TypeScript 5** - Typed superset of JavaScript -- **Tailwind CSS 4** - Utility-first CSS framework -- **Lucide React** - Beautiful & consistent icon toolkit -- **Next Themes** - Perfect dark mode in 2 lines of code +- **Next.js 15** (App Router) and **TypeScript 5** +- **shadcn/ui** (Radix primitives) with **Tailwind CSS 4**, monochrome theme tokens in `src/app/globals.css` +- **Geist Sans / Geist Mono** fonts, **Lucide** icons, **cmdk** command menu, **sonner** toasts +- **SWR** for client data fetching, **next-themes** for dark/light ### Backend -- **Next.js API Routes** - Serverless API endpoints -- **MongoDB** - NoSQL database for data storage -- **Mongoose** - MongoDB object modeling for Node.js -- **NextAuth.js** - Complete open source authentication solution - -### Security & Utilities -- **bcryptjs** - Password hashing library -- **crypto-js** - Cryptographic algorithms for data encryption -- **rate-limiter-flexible** - Rate limiting middleware -- **custom-uuid** - UUID generation for unique identifiers - -### Development Tools -- **ESLint** - JavaScript linter -- **PostCSS** - Tool for transforming CSS with JavaScript -- **Autoprefixer** - PostCSS plugin to parse CSS and add vendor prefixes - -## Dependencies - -### Production Dependencies -- next (^15.3.5) -- react (18.2.0) -- react-dom (18.2.0) -- mongoose (^8.15.1) -- next-auth (^4.24.11) -- bcryptjs (^3.0.2) -- crypto-js (^4.2.0) -- tailwindcss (^4.0.0) -- next-themes (^0.4.6) -- lucide-react (^0.522.0) -- rate-limiter-flexible (^7.1.1) -- sonner (^2.0.6) -- custom-uuid (^3.1.1) -- clsx (^2.1.1) - -### Development Dependencies -- typescript (^5) -- @types/node (^20) -- @types/react (^19) -- @types/react-dom (^19) -- @types/crypto-js (^4.2.2) -- eslint (^8.56.0) -- eslint-config-next (15.3.2) +- **Next.js route handlers** under `src/app/api` +- **MongoDB** with **Mongoose** +- **NextAuth.js** (Google + email/password) with an optional passkey or TOTP second factor + +### Security +- **SimpleWebAuthn** for passkeys (only public keys stored, single-use challenges) +- **otpauth** + **qrcode** for TOTP enrollment and verification (replay-protected, rate-limited) +- **bcryptjs** for password hashing; backup codes and API tokens are stored as SHA-256 hashes +- **Node `crypto` AES-256-GCM** for values at rest (`crypto-js` only to read legacy values) +- **zod** request validation, **rate-limiter-flexible** rate limiting ## Running ### Prerequisites -- Node.js (v18 or higher) +- Node.js 20.12 or newer - MongoDB database - npm or yarn package manager @@ -99,8 +84,8 @@ The Environment Sharing Platform is a secure web application that enables develo 1. Clone the repository: ```bash -git clone https://github.com/GDGVIT/env-sharing.git -cd env-sharing/web-app +git clone https://github.com/GDGVIT/envoy.git +cd envoy/web-app ``` 2. Install dependencies: @@ -122,13 +107,17 @@ NEXTAUTH_SECRET=your_nextauth_secret GOOGLE_CLIENT_ID=your_google_client_id GOOGLE_CLIENT_SECRET=your_google_client_secret -# Encryption -NEXT_PUBLIC_ENCRYPTION_SECRET=your_32_character_encryption_key +# Encryption (server only; use a long random value and keep it backed up) +ENCRYPTION_SECRET=your_long_random_encryption_key #Others NEXT_PUBLIC_APP_NAME=your_app_name ``` +`ENCRYPTION_SECRET` is used only by server-side code and must stay the same for as long as encrypted data needs to be read: it encrypts variable values, temporary links and authenticator-app secrets. Changing it makes that data unreadable (nothing is deleted; restoring the old value fixes it), so keep it backed up. + +Optional: `WEBAUTHN_RP_ID` and `WEBAUTHN_ORIGIN` pin passkeys to one domain (e.g. `envoy.example.com` and `https://envoy.example.com`). Without them, passkeys belong to whichever domain they were created on. + ### Development Start the development server: @@ -146,34 +135,66 @@ npm run build npm start ``` -### Linting +### Linting and type checks -Run ESLint to check for code quality issues: ```bash npm run lint +npm run typecheck ``` +## CLI & API access + +1. In the app, open **Account → API tokens** and create a token. +2. Fetch an environment (every project page has a **Use from CLI** button with the exact URL): + +```bash +export ENVOY_TOKEN=envoy_pat_… + +# write a .env file +curl -fsS -H "Authorization: Bearer $ENVOY_TOKEN" \ + "https://your-envoy-host/api/v1////?format=dotenv" > .env + +# or get JSON: { "vars": { "KEY": "value" }, "version": 3, … } +curl -fsS -H "Authorization: Bearer $ENVOY_TOKEN" \ + "https://your-envoy-host/api/v1////" +``` + +Tokens act with the owner's *current* access: removing someone from a team (or the org) immediately cuts off their tokens too. Revoke a token from the same page. + +### Pagination + +List endpoints (organizations, members, team projects and members, invite links, API tokens, audit log) return one page at a time: + +- `?limit=` sets the page size: default 50, at most 100. +- Each response includes `nextCursor`. Pass it back as `?cursor=` to get the next page; it is `null` on the last page. +- Cursors are opaque and stay stable when items are added or removed between requests. +- Some lists also return `total`. Org members accept `?q=` to search by name or email. + +## Breaking changes from the workspaces/groups version + +Envoy used to organize variables as Workspaces → Groups. Existing data has been moved to Organizations → Teams → Projects → Environments. +- Group tokens no longer work. `GET /api/env/get` returns `410 Gone`; use personal API tokens and `/api/v1/…`. +- Nothing can be made public. Use invite links, or temporary links (at most 24 hours) for one-off sharing. +- Invitations are 10-character codes (`XXXXX-XXXXX`), also usable as an `/invite/` link. Envoy doesn't send email invites. + ## Project Structure ``` web-app/ ├── src/ -│ ├── app/ # App router pages and API routes -│ │ ├── api/ # API endpoints -│ │ │ ├── (auth)/ # Authentication routes -│ │ │ └── (user)/ # User-related routes -│ │ ├── dashboard/ # Dashboard pages -│ │ ├── login/ # Login page -│ │ └── signup/ # Signup page -│ ├── components/ # Reusable React components -│ │ ├── dashboard/ # Dashboard-specific components -│ │ └── misc/ # Miscellaneous components -│ ├── models/ # MongoDB schemas -│ ├── providers/ # React context providers -│ ├── types/ # TypeScript type definitions -│ └── utils/ # Utility functions -├── public/ # Static assets -└── package.json # Project dependencies and scripts +│ ├── app/ +│ │ ├── (auth)/ # login, signup, 2FA setup/verify, onboarding, invite links +│ │ ├── (app)/ # signed-in app: /o/[org]/…, /account +│ │ ├── share/ # temporary links (expire within 24h) +│ │ └── api/ # route handlers (orgs, teams, projects, envs, invites, tokens, 2fa, v1) +│ ├── components/ +│ │ ├── ui/ # shadcn/ui components +│ │ ├── app/ # app shell, env editor, command menu +│ │ └── landing/ # marketing page +│ ├── lib/ # auth, authorization, audit, totp, validation, dotenv +│ ├── models/ # Mongoose schemas +│ └── utils/ # encryption, rate limiting, db connection +└── public/ ``` ## Contributors @@ -214,5 +235,5 @@ web-app/

- Made with ❤ by GDSC-VIT + Made with ❤ by GDG VIT

diff --git a/web-app/components.json b/web-app/components.json new file mode 100644 index 0000000..2a42785 --- /dev/null +++ b/web-app/components.json @@ -0,0 +1,25 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "radix-nova", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "", + "css": "src/app/globals.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "iconLibrary": "lucide", + "rtl": false, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "menuColor": "default", + "menuAccent": "subtle", + "registries": {} +} diff --git a/web-app/next.config.ts b/web-app/next.config.ts index e9ffa30..fdf9b28 100644 --- a/web-app/next.config.ts +++ b/web-app/next.config.ts @@ -1,7 +1,11 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { - /* config options here */ + // Keep the dev-only Next.js indicator away from the sidebar account menu (bottom-left). + devIndicators: { position: "bottom-right" }, + // Loaded by Node at runtime instead of bundled: the package lists optional storage backends + // (e.g. drizzle-orm) that Envoy doesn't use, and bundling it fails to resolve them. + serverExternalPackages: ["rate-limiter-flexible"], }; export default nextConfig; diff --git a/web-app/package-lock.json b/web-app/package-lock.json index 894958a..cd56876 100644 --- a/web-app/package-lock.json +++ b/web-app/package-lock.json @@ -8,31 +8,50 @@ "name": "web-app", "version": "1.0.0", "dependencies": { + "@simplewebauthn/browser": "^13.3.0", + "@simplewebauthn/server": "^13.3.3", "autoprefixer": "^10.4.21", "bcryptjs": "^3.0.2", + "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "cmdk": "^1.1.1", + "cn": "^0.5.2", "crypto-js": "^4.2.0", "custom-uuid": "^3.1.1", + "date-fns": "^4.4.0", + "geist": "^1.9.2", + "input-otp": "^1.5.0", "lightningcss": "^1.30.1", "lucide-react": "^0.522.0", - "mongoose": "^8.15.1", - "next": "^15.3.5", - "next-auth": "^4.24.11", + "mongoose": "^8.24.8", + "next": "^15.5.29", + "next-auth": "^4.24.15", "next-themes": "^0.4.6", + "otpauth": "^9.6.0", + "qrcode": "^1.5.4", + "radix-ui": "^1.8.1", "rate-limiter-flexible": "^7.1.1", "react": "18.2.0", + "react-day-picker": "^10.0.2", "react-dom": "18.2.0", - "sonner": "^2.0.6" + "server-only": "^0.0.1", + "shadcn": "^4.23.0", + "simple-icons": "^16.36.0", + "sonner": "^2.0.6", + "swr": "^2.5.1", + "tw-animate-css": "^1.4.0", + "zod": "^4.6.7" }, "devDependencies": { "@eslint/eslintrc": "^3", "@tailwindcss/postcss": "^4.1.11", "@types/crypto-js": "^4.2.2", "@types/node": "^20", + "@types/qrcode": "^1.5.6", "@types/react": "^19", "@types/react-dom": "^19", - "eslint": "^8.56.0", - "eslint-config-next": "15.3.2", + "eslint": "^9.39.5", + "eslint-config-next": "^15.5.29", "tailwindcss": "^4.0.0", "typescript": "^5" } @@ -64,856 +83,3743 @@ "node": ">=6.0.0" } }, - "node_modules/@babel/runtime": { - "version": "7.28.2", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.2.tgz", - "integrity": "sha512-KHp2IflsnGywDjBWDkR9iEqiWSpc8GIi0lgTT3mOElT0PP1tG26P4tmFI2YvAdzgq9RGyoHZQEIEdZy6Ec5xCA==", + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@emnapi/core": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.5.tgz", - "integrity": "sha512-XsLw1dEOpkSX/WucdqUhPWP7hDxSvZiY+fsUC14h+FtQ2Ifni4znbBt8punRX+Uj2JG/uDb8nEHVKvrVlvdZ5Q==", - "dev": true, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.0.4", - "tslib": "^2.4.0" + "engines": { + "node": ">=6.9.0" } }, - "node_modules/@emnapi/runtime": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.5.tgz", - "integrity": "sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg==", + "node_modules/@babel/core": { + "version": "7.29.13", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.13.tgz", + "integrity": "sha512-j1nvOQCZYSgp7HLmisnCXFTpVvFODiijQ9QHkORfGKWH6YDtskTEeCJS7eV545GZ9UBzrq0e2xsOd5QQ5PjIqw==", "license": "MIT", - "optional": true, "dependencies": { - "tslib": "^2.4.0" + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.13", + "@babel/helper-compilation-targets": "^7.29.13", + "@babel/helper-module-transforms": "^7.29.13", + "@babel/helpers": "^7.29.13", + "@babel/parser": "^7.29.13", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.13", + "@babel/types": "^7.29.11", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" } }, - "node_modules/@emnapi/wasi-threads": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.0.4.tgz", - "integrity": "sha512-PJR+bOmMOPH8AtcTGAyYNiuJ3/Fcoj2XN/gBEWzDIKh254XO+mM9XoXHk5GNEhodxeMznbg7BlRojVbKN+gC6g==", - "dev": true, + "node_modules/@babel/core/node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" } }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", - "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", - "dev": true, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.2.tgz", + "integrity": "sha512-GYrCADwpu6yV5OAvchkQ27Z5wwMxWAEMg5NNWjyHh/5DboFFqbdbvYNIvZouQpxXyeMPoTOh0vdPYKlf0ZswdQ==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.13", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.13.tgz", + "integrity": "sha512-T5FRo+RzVHvGjWOuF1Ljc44b+NXAOazodHkGUHp4I3zCkQ4/sNVJm56h1S278LMR0bY5xBhztGziSbli8zObIw==", "license": "MIT", "dependencies": { - "eslint-visitor-keys": "^3.4.3" + "@babel/parser": "^7.29.13", + "@babel/types": "^7.29.11", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.29.7.tgz", + "integrity": "sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + "engines": { + "node": ">=6.9.0" } }, - "node_modules/@eslint-community/regexpp": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", - "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", - "dev": true, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.13", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.13.tgz", + "integrity": "sha512-8WxJ4/Vmes3ghacj8VOJy3NW8eyHiFY/4thU/LSBuBE470+igjR/j1ZTNn8fV5VdNrLx6iXUwig526UdOvfP0w==", "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + "node": ">=6.9.0" } }, - "node_modules/@eslint/eslintrc": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", - "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", - "dev": true, + "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.2.tgz", + "integrity": "sha512-GYrCADwpu6yV5OAvchkQ27Z5wwMxWAEMg5NNWjyHh/5DboFFqbdbvYNIvZouQpxXyeMPoTOh0vdPYKlf0ZswdQ==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "license": "ISC" + }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.29.13", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.29.13.tgz", + "integrity": "sha512-fo8/Fr44/DMV71FQjk/HA0Npmu3bo5DFoWTCW1dhUR8Vx3NFfAVeEy0TbcBpt4e+hd2QhdmWgmspHo/9iWj8RA==", "license": "MIT", "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^10.0.1", - "globals": "^14.0.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-member-expression-to-functions": "^7.29.7", + "@babel/helper-optimise-call-expression": "^7.29.7", + "@babel/helper-replace-supers": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.13", + "@babel/traverse": "^7.29.13", + "semver": "^6.3.1" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">=6.9.0" }, - "funding": { - "url": "https://opencollective.com/eslint" + "peerDependencies": { + "@babel/core": "^7.0.0" } }, - "node_modules/@eslint/js": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", - "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", - "dev": true, + "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": { + "version": "6.3.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.2.tgz", + "integrity": "sha512-GYrCADwpu6yV5OAvchkQ27Z5wwMxWAEMg5NNWjyHh/5DboFFqbdbvYNIvZouQpxXyeMPoTOh0vdPYKlf0ZswdQ==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", "license": "MIT", "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": ">=6.9.0" } }, - "node_modules/@humanwhocodes/config-array": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", - "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", - "deprecated": "Use @eslint/config-array instead", - "dev": true, - "license": "Apache-2.0", + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.29.7.tgz", + "integrity": "sha512-j+7JYmk1JYDtACIGj0QJqqWZjoUpMoEikQGADMaHgCMCSDqd2+P32rfcibUNrGOMWrlzK1WJBdxrB3JJQZwWtg==", + "license": "MIT", "dependencies": { - "@humanwhocodes/object-schema": "^2.0.3", - "debug": "^4.3.1", - "minimatch": "^3.0.5" + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { - "node": ">=10.10.0" + "node": ">=6.9.0" } }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.22" + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" + "engines": { + "node": ">=6.9.0" } }, - "node_modules/@humanwhocodes/object-schema": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", - "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", - "deprecated": "Use @eslint/object-schema instead", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/@img/sharp-darwin-arm64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.3.tgz", - "integrity": "sha512-ryFMfvxxpQRsgZJqBd4wsttYQbCxsJksrv9Lw/v798JcQ8+w84mBWuXwl+TT0WJ/WrYOLaYpwQXi3sA9nTIaIg==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.13", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.13.tgz", + "integrity": "sha512-/U96wOz+SCoZ7Da/8kjDGltcNY280JPQcRNPK0Hk3PTbdpaiBDQImtC6JsbVwb1HGMr3usZQU2WsWVjhYNmRzw==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.13" + }, "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=6.9.0" }, - "funding": { - "url": "https://opencollective.com/libvips" + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.29.7.tgz", + "integrity": "sha512-+kmGVjcT9RGYzoDwdwEqEvGgKe3BYq+O1iGzjFubaNgZHwYHP6lsF2Yghf4kEuv9BV7tYDZ913aBW9am6YKong==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.2.0" + "engines": { + "node": ">=6.9.0" } }, - "node_modules/@img/sharp-darwin-x64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.3.tgz", - "integrity": "sha512-yHpJYynROAj12TA6qil58hmPmAwxKKC7reUqtGLzsOHfP7/rniNGTL8tjWX6L3CTV4+5P4ypcS7Pp+7OB+8ihA==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "license": "MIT", "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.29.7.tgz", + "integrity": "sha512-atfGXWSeCiF4DnKZIfmJfQRkSw9b9gNNXR1kqKjbhG4pGYCOnkp8OcTB8E3NXjBu8NpheSnOeNKz8KT7UNFTmQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-member-expression-to-functions": "^7.29.7", + "@babel/helper-optimise-call-expression": "^7.29.7", + "@babel/traverse": "^7.29.7" }, - "funding": { - "url": "https://opencollective.com/libvips" + "engines": { + "node": ">=6.9.0" }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.2.0" + "peerDependencies": { + "@babel/core": "^7.0.0" } }, - "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.0.tgz", - "integrity": "sha512-sBZmpwmxqwlqG9ueWFXtockhsxefaV6O84BMOrhtg/YqbTaRdqDE7hxraVE3y6gVM4eExmfzW4a8el9ArLeEiQ==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "darwin" - ], - "funding": { - "url": "https://opencollective.com/libvips" + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.29.13", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.29.13.tgz", + "integrity": "sha512-Abm4rKIH0tmCayJLwcoeOkdzpYUphTbO52fHhyEBHn7S0R+ni4epNLI/8nq1L0DN2Bl0u5qmgWm2Og/z4fBb+Q==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.13", + "@babel/types": "^7.29.11" + }, + "engines": { + "node": ">=6.9.0" } }, - "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.0.tgz", - "integrity": "sha512-M64XVuL94OgiNHa5/m2YvEQI5q2cl9d/wk0qFTDVXcYzi43lxuiFTftMR1tOnFQovVXNZJ5TURSDK2pNe9Yzqg==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "darwin" - ], - "funding": { - "url": "https://opencollective.com/libvips" + "node_modules/@babel/helper-string-parser": { + "version": "7.29.13", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.13.tgz", + "integrity": "sha512-fWegr+WsEsiMVW4KUNdIIzzMTZSzNiqhy0//6+2MRi2d5NFNsL/AYwBQW8eBnRbTBVRnDVzjaJEUqKaCWjMMdQ==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" } }, - "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.0.tgz", - "integrity": "sha512-mWd2uWvDtL/nvIzThLq3fr2nnGfyr/XMXlq8ZJ9WMR6PXijHlC3ksp0IpuhK6bougvQrchUAfzRLnbsen0Cqvw==", - "cpu": [ - "arm" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" } }, - "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.0.tgz", - "integrity": "sha512-RXwd0CgG+uPRX5YYrkzKyalt2OJYRiJQ8ED/fi1tq9WQW2jsQIn0tqrlR5l5dr/rjqq6AHAxURhj2DVjyQWSOA==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" } }, - "node_modules/@img/sharp-libvips-linux-ppc64": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.0.tgz", - "integrity": "sha512-Xod/7KaDDHkYu2phxxfeEPXfVXFKx70EAFZ0qyUdOjCcxbjqyJOEUpDe6RIyaunGxT34Anf9ue/wuWOqBW2WcQ==", - "cpu": [ - "ppc64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" + "node_modules/@babel/helpers": { + "version": "7.29.13", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.13.tgz", + "integrity": "sha512-laoRpD/kO/iA4CDlgJbixHekg7oPvWALK0PQyK3Ob2I2njZRvVAD77RfWCIImtqWCq6KijoBoZK7bp/HlTWASQ==", + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.11" + }, + "engines": { + "node": ">=6.9.0" } }, - "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.0.tgz", - "integrity": "sha512-eMKfzDxLGT8mnmPJTNMcjfO33fLiTDsrMlUVcp6b96ETbnJmd4uvZxVJSKPQfS+odwfVaGifhsB07J1LynFehw==", - "cpu": [ - "s390x" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" + "node_modules/@babel/parser": { + "version": "7.29.13", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.13.tgz", + "integrity": "sha512-fJM50YYjlTiBhgdkGmC1tBpv+FMAyo4annf2X6Aks+0yRlaiVkYW/3f07xAfVVkYIoZ9fFZp+mEThUFnuybhkQ==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.11" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" } }, - "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.0.tgz", - "integrity": "sha512-ZW3FPWIc7K1sH9E3nxIGB3y3dZkpJlMnkk7z5tu1nSkBoCgw2nSRTFHI5pB/3CQaJM0pdzMF3paf9ckKMSE9Tg==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.29.7.tgz", + "integrity": "sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.0.tgz", - "integrity": "sha512-UG+LqQJbf5VJ8NWJ5Z3tdIe/HXjuIdo4JeVNADXBFuG7z9zjoegpzzGIyV5zQKi4zaJjnAd2+g2nna8TZvuW9Q==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.29.7.tgz", + "integrity": "sha512-ngr+82Sh0xMz25TPCZi+nC2iTzjfCdWS2ONXTp/PtSCHCgaCNBpdMqgvJ2ccdLlClVZ7sisIgB914j/JFe+RZA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.0.tgz", - "integrity": "sha512-SRYOLR7CXPgNze8akZwjoGBoN1ThNZoqpOgfnOxmWsklTGVfJiGJoC/Lod7aNMGA1jSsKWM1+HRX43OP6p9+6Q==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.29.7.tgz", + "integrity": "sha512-j0vCldybPC5b5dwCQOJ21uKtHzt7hxLygJTg9eF1ScfaikEDNfzn94XoW5Fi+seBR0nCyL23xaBFFkq7dTM8XQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@img/sharp-linux-arm": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.3.tgz", - "integrity": "sha512-oBK9l+h6KBN0i3dC8rYntLiVfW8D8wH+NPNT3O/WBHeW0OQWCjfWksLUaPidsrDKpJgXp3G3/hkmhptAW0I3+A==", - "cpu": [ - "arm" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], + "node_modules/@babel/plugin-transform-typescript": { + "version": "7.29.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.29.9.tgz", + "integrity": "sha512-FFwIwzU+7SCOuxxV4YtJql6T9981ZVTm+FHO5GhVsRqCTdy0WwrEZh3l42ARpXruJUDGOptdduHW6Zr7pNPLNg==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", + "@babel/plugin-syntax-typescript": "^7.29.7" + }, "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=6.9.0" }, - "funding": { - "url": "https://opencollective.com/libvips" + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-typescript": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.29.7.tgz", + "integrity": "sha512-/Foi8vKY2EVbed/1eZx0gJEEwHAIxogrySI7rULcRIvhZzbvoE/b5qG5Ghc0WKAFKOHA9SD1x7RsFlOYdutIiQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "@babel/plugin-syntax-jsx": "^7.29.7", + "@babel/plugin-transform-modules-commonjs": "^7.29.7", + "@babel/plugin-transform-typescript": "^7.29.7" }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.2.0" + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@img/sharp-linux-arm64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.3.tgz", - "integrity": "sha512-QdrKe3EvQrqwkDrtuTIjI0bu6YEJHTgEeqdzI3uWJOH6G1O8Nl1iEeVYRGdj1h5I21CqxSvQp1Yv7xeU3ZewbA==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], + "node_modules/@babel/runtime": { + "version": "7.28.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.2.tgz", + "integrity": "sha512-KHp2IflsnGywDjBWDkR9iEqiWSpc8GIi0lgTT3mOElT0PP1tG26P4tmFI2YvAdzgq9RGyoHZQEIEdZy6Ec5xCA==", + "license": "MIT", "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" }, - "funding": { - "url": "https://opencollective.com/libvips" + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.13", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.13.tgz", + "integrity": "sha512-0ve0xMlFA73dgY1oyJRY6RoW+/bncdcOQqg6jjYcVfXBK90OdLXjm9R6lmRetdZVrPjXkKIoMVBqYFPjcAlZ4Q==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.13", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.13", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.11", + "debug": "^4.3.1" }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.2.0" + "engines": { + "node": ">=6.9.0" } }, - "node_modules/@img/sharp-linux-ppc64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.3.tgz", - "integrity": "sha512-GLtbLQMCNC5nxuImPR2+RgrviwKwVql28FWZIW1zWruy6zLgA5/x2ZXk3mxj58X/tszVF69KK0Is83V8YgWhLA==", - "cpu": [ - "ppc64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], + "node_modules/@babel/types": { + "version": "7.29.11", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.11.tgz", + "integrity": "sha512-WCsemW40Cpc6gouC1SrY6PBaZ+oLRlEVY9XBvZBI7il/HyUrb2nJ505wE1MHEFyDZNnrWDEuy4jL50WVHfTTSQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.11", + "@babel/helper-validator-identifier": "^7.29.7" + }, "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=6.9.0" + } + }, + "node_modules/@date-fns/tz": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@date-fns/tz/-/tz-1.5.0.tgz", + "integrity": "sha512-lwYN/vDPeNRULcepoE/LO2Pgx+7/RV+S9ARfbc9lr2DtGkOD7pAiruHvbR1RX3Qyf6ja47EWJDMsNK5vK08DJg==", + "license": "MIT" + }, + "node_modules/@dotenvx/dotenvx": { + "version": "1.75.1", + "resolved": "https://registry.npmjs.org/@dotenvx/dotenvx/-/dotenvx-1.75.1.tgz", + "integrity": "sha512-/BITOC9dmS/edY2zQwZNicQ059O6RKabtQfyEafV0nGtfYRNHYy1DIPiYVcov40+tob9hfmBnbR963dS+EQ1DQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@dotenvx/primitives": "^0.8.0", + "commander": "^11.1.0", + "conf": "^10.2.0", + "dotenv": "^17.2.1", + "enquirer": "^2.4.1", + "env-paths": "^2.2.1", + "execa": "^5.1.1", + "fdir": "^6.2.0", + "ignore": "^5.3.0", + "object-treeify": "1.1.33", + "open": "^8.4.2", + "picomatch": "^4.0.4", + "systeminformation": "^5.22.11", + "undici": "^7.11.0", + "which": "^4.0.0", + "yocto-spinner": "^1.1.0" }, - "funding": { - "url": "https://opencollective.com/libvips" + "bin": { + "dotenvx": "src/cli/dotenvx.js" }, - "optionalDependencies": { - "@img/sharp-libvips-linux-ppc64": "1.2.0" + "funding": { + "url": "https://dotenvx.com" } }, - "node_modules/@img/sharp-linux-s390x": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.3.tgz", - "integrity": "sha512-3gahT+A6c4cdc2edhsLHmIOXMb17ltffJlxR0aC2VPZfwKoTGZec6u5GrFgdR7ciJSsHT27BD3TIuGcuRT0KmQ==", - "cpu": [ - "s390x" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], + "node_modules/@dotenvx/dotenvx/node_modules/commander": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/@dotenvx/dotenvx/node_modules/define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@dotenvx/dotenvx/node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=10" }, "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.2.0" + "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, - "node_modules/@img/sharp-linux-x64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.3.tgz", - "integrity": "sha512-8kYso8d806ypnSq3/Ly0QEw90V5ZoHh10yH0HnrzOCr6DKAPI6QVHvwleqMkVQ0m+fc7EH8ah0BB0QPuWY6zJQ==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], + "node_modules/@dotenvx/dotenvx/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=12.0.0" }, - "funding": { - "url": "https://opencollective.com/libvips" + "peerDependencies": { + "picomatch": "^3 || ^4" }, - "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.2.0" + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } } }, - "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.3.tgz", - "integrity": "sha512-vAjbHDlr4izEiXM1OTggpCcPg9tn4YriK5vAjowJsHwdBIdx0fYRsURkxLG2RLm9gyBq66gwtWI8Gx0/ov+JKQ==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], + "node_modules/@dotenvx/dotenvx/node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "license": "MIT", "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=10" }, "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.2.0" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.3.tgz", - "integrity": "sha512-gCWUn9547K5bwvOn9l5XGAEjVTTRji4aPTqLzGXHvIr6bIDZKNTA34seMPgM0WmSf+RYBH411VavCejp3PkOeQ==", - "cpu": [ - "x64" - ], + "node_modules/@dotenvx/dotenvx/node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.2.0" + "node": ">=10.17.0" } }, - "node_modules/@img/sharp-wasm32": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.3.tgz", - "integrity": "sha512-+CyRcpagHMGteySaWos8IbnXcHgfDn7pO2fiC2slJxvNq9gDipYBN42/RagzctVRKgxATmfqOSulgZv5e1RdMg==", - "cpu": [ - "wasm32" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", - "optional": true, - "dependencies": { - "@emnapi/runtime": "^1.4.4" + "node_modules/@dotenvx/dotenvx/node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "license": "MIT", + "bin": { + "is-docker": "cli.js" }, "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=8" }, "funding": { - "url": "https://opencollective.com/libvips" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@img/sharp-win32-arm64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.3.tgz", - "integrity": "sha512-MjnHPnbqMXNC2UgeLJtX4XqoVHHlZNd+nPt1kRPmj63wURegwBhZlApELdtxM2OIZDRv/DFtLcNhVbd1z8GYXQ==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], + "node_modules/@dotenvx/dotenvx/node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=8" }, "funding": { - "url": "https://opencollective.com/libvips" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@img/sharp-win32-ia32": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.3.tgz", - "integrity": "sha512-xuCdhH44WxuXgOM714hn4amodJMZl3OEvf0GVTm0BEyMeA2to+8HEdRPShH0SLYptJY1uBw+SCFP9WVQi1Q/cw==", - "cpu": [ - "ia32" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], + "node_modules/@dotenvx/dotenvx/node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@dotenvx/dotenvx/node_modules/isexe": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.5.tgz", + "integrity": "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@dotenvx/dotenvx/node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@dotenvx/dotenvx/node_modules/open": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "license": "MIT", + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=12" }, "funding": { - "url": "https://opencollective.com/libvips" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@img/sharp-win32-x64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.3.tgz", - "integrity": "sha512-OWwz05d++TxzLEv4VnsTz5CmZ6mI6S05sfQGEMrNrQcOEERbX46332IvE7pO/EUiw7jUrrS40z/M7kPyjfl04g==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", + "node_modules/@dotenvx/dotenvx/node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@dotenvx/dotenvx/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, + "node_modules/@dotenvx/dotenvx/node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/@dotenvx/dotenvx/node_modules/which": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", + "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^16.13.0 || >=18.0.0" + } + }, + "node_modules/@dotenvx/primitives": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@dotenvx/primitives/-/primitives-0.8.0.tgz", + "integrity": "sha512-VYJy0uhFm9zTJ1TxBaW/pA8bjbOM/OttaNMwZ1RHG4JKyRG7DhSdiqD1ipQoAyoD22olUtxbP78W9xY3Wd11bg==", + "license": "BSD-3-Clause" + }, + "node_modules/@emnapi/core": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.5.tgz", + "integrity": "sha512-XsLw1dEOpkSX/WucdqUhPWP7hDxSvZiY+fsUC14h+FtQ2Ifni4znbBt8punRX+Uj2JG/uDb8nEHVKvrVlvdZ5Q==", + "dev": true, + "license": "MIT", "optional": true, - "os": [ - "win32" - ], + "dependencies": { + "@emnapi/wasi-threads": "1.0.4", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz", + "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.0.4.tgz", + "integrity": "sha512-PJR+bOmMOPH8AtcTGAyYNiuJ3/Fcoj2XN/gBEWzDIKh254XO+mM9XoXHk5GNEhodxeMznbg7BlRojVbKN+gC6g==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", + "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, "funding": { - "url": "https://opencollective.com/libvips" + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, - "node_modules/@isaacs/fs-minipass": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", - "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", "dev": true, - "license": "ISC", + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "dev": true, + "license": "Apache-2.0", "dependencies": { - "minipass": "^7.0.4" + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" }, "engines": { - "node": ">=18.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.12", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz", - "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==", + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.7.tgz", + "integrity": "sha512-F42g89Qd5oAWtp0k0nnSrjziAKza7w8SVT4mStc18LZMaRb4J1HQAHLCalEtDCxrTuksx7NU9qsmeLwpOfPqWw==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.3.2", + "minimatch": "^3.1.5", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.5", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.5.tgz", + "integrity": "sha512-QywQuszQh77pIXCsq998c8hbhSTI/azTty1Z6N53dmAudKHhy573j3yvRLsX2BSp8YpLtoCEG8E9DJe+8zUh4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.8.0.tgz", + "integrity": "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.12" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.8.0.tgz", + "integrity": "sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.8.0", + "@floating-ui/utils": "^0.2.12" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.9.tgz", + "integrity": "sha512-JDjEFGCpImxDCA7JJKviA0M9+RtmJdj0m/NVU5IMgBK+AmZouAQQ7/+2GLH0GXXY0YMw9oXPB8hKdbPYg5QLYg==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.8.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.12.tgz", + "integrity": "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==", + "license": "MIT" + }, + "node_modules/@hexagon/base64": { + "version": "1.1.28", + "resolved": "https://registry.npmjs.org/@hexagon/base64/-/base64-1.1.28.tgz", + "integrity": "sha512-lhqDEAvWixy3bZ+UOYbPwUbBkwBq5C1LAJ/xPC8Oi+lL54oyakv/npbA0aU2hgCsx/1NUd4IBvV03+aUBWxerw==", + "license": "MIT" + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.5.tgz", + "integrity": "sha512-QRUlFQ0WxvdWyqqG/WtI3iupfD5rBzmCHXSdPsY91sAtVtTo7Q4cb6zOccZ3gqEqkr0f1As1ehLqmEpDsRf+lg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.3.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.5.tgz", + "integrity": "sha512-+BR255RhDlpygUpOc/Jdt1nT6DQ3XG/ERo5wbcdOf5Q320dKtPCKPLR1LJs9VGXRaMa8l1uUa0tkCNOXiAxZUw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.3.4" + } + }, + "node_modules/@img/sharp-freebsd-wasm32": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.5.tgz", + "integrity": "sha512-Y/z91nEZ4uIBX5X3nfTovjU9lHNKFYbL2lpHCLVNmXQK03VIZvXBBt0KxbPGp2SdGSF+2mQU4e+hQaWOt86iAw==", + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "dependencies": { + "@img/sharp-wasm32": "0.35.5" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.4.tgz", + "integrity": "sha512-5R89nBYiRdUlSWJxPhO+GVtaXzXSxKnRu/xqMn3KTA3L9EB9Oy/P+Nn2f2vlhPuUdy/Zusb2DarbyTpGCfEDuw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.4.tgz", + "integrity": "sha512-iR2OKH80yi0U+dUplyh3/xdpFvps6YkCwsXenIJxqxR1v9o+xtKTGbS9H7cps+2Vxjc8B1j96p75NmTGjIhtpQ==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.4.tgz", + "integrity": "sha512-LmRtTsOHuvM2+wlO2Db37dx5MiZhB0FvSunciw48YjdOkZz9KAiRbm8ujeMOA1INqmei5NapFxYEK1D1ZSidmw==", + "cpu": [ + "arm" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.4.tgz", + "integrity": "sha512-Y3dgX/6lE2QhQb+Gxy0WZxfg9MEm/JBjamZpS2IklP7xIQoKN4hzAm7KcMVGtaVDt3neE9OKBC7vAfonA/Lr1A==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.4.tgz", + "integrity": "sha512-Le6boB8Tai0Nis+gIxIpKx68UDVVIqdR8Tin5Yf1z2LJJQLDJvCDRqRu+jC2qCoD+eIomonmOwB4smBRxfVpYQ==", + "cpu": [ + "ppc64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.4.tgz", + "integrity": "sha512-aHkkIEHPRdQEegJN20MLmGtxYD9R2wQr3Cwpddnu5+YKMt6Uzax7S9h5gpZTo8wyrGuZSlfQ63OevL5mTyOC7Q==", + "cpu": [ + "riscv64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.4.tgz", + "integrity": "sha512-ra/mB6MikESDUO7Yg+Mi95bFBb9GsObURuhnOv3OqknjGe9sZrG8tCe9q0xSIGrtLgvgw0gKnFWcK4blSgQOuQ==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.4.tgz", + "integrity": "sha512-GJ//SSXbnwSDes02umB3nDJLFcQzw8a18V8fyhqr6tV515tOEMdImjjxj1AoafMRz56F3PHgftnj1QEKSU1zkw==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.4.tgz", + "integrity": "sha512-hvulFwtjUcagsis6BBxHwGFwWoNZjgYmULGVrZcyfNbjA8hKILbRxGg15/7w5HDyXHXUos/j6baAWqnCyQ2DWA==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.4.tgz", + "integrity": "sha512-6zXKeE/p39I1AmA3cJG35eyBGNqNddLnUXjhwBnsGjFPWqf5VKkDBEqaEkPDoTEtkxwi2vv8Tcr2mDyP4So7Fg==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.5.tgz", + "integrity": "sha512-LEaXK2WdXVK5ykcw0buWyPMsmLLL2vpHLD6yrNSW+JGEL3BZPA4tpKN6iaMc4AxTTAoaX/sU1rOL51lcIz48ZQ==", + "cpu": [ + "arm" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.3.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.5.tgz", + "integrity": "sha512-LYVx5JTsOM2CBzmxreh+nl64/3H6Xb09iSLknqH47z2T2DFFxDeFLP5y4dJwe6H7uGQlHPyEEtIqyo3DYsRwdQ==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.3.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.5.tgz", + "integrity": "sha512-QVxAAq8evVRI9ia2vqgwrmWucn5Dfv+JdWzj75pD8omHLPSP7f8p20O8jxzjCcuCEQEOtYOZUmX1hkiZ0kdevA==", + "cpu": [ + "ppc64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.3.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.5.tgz", + "integrity": "sha512-LtdreXguaavKODPIfzJ4kffx7UNt1omwtK0rch4EBbbSTXPnxWmYSayXdLJw0fJzQ97kHt1gL/yh4tvU+nCyRQ==", + "cpu": [ + "riscv64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.3.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.5.tgz", + "integrity": "sha512-UZasTOFiYzotTsGOCu42BfUzP6Tu6Do/947iRm1RsLKvlllxwGcn4RN27LibGWceix4Y+Pmw3jsnTcCQIgWjqA==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.3.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.5.tgz", + "integrity": "sha512-SxFtLTeJInhAA9Q836kux2vZNeOBQEx658qvbboZScr0wIARym3IcGmW7KpVD5sbVg0Ojy+udFQdayYIZyoNog==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.3.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.5.tgz", + "integrity": "sha512-9HbMclmI1zlNkFRs3z9/eBtDjfD0sGlrX1z6b1qwmiFY5ElDLh4BC0LPBdVp7z1DXFiKlIcznf+ZlsuZzLxQqg==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.3.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.5.tgz", + "integrity": "sha512-4KOphqB035HrVdqLZfCgMzzERrQkkzOwRhl4OAkRO1YCldbaFjySXMaK534Mo0V+LndnlJk+sbUyLeU0ULyD1A==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.3.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.5.tgz", + "integrity": "sha512-Ptsga1su4tQx+LLF1ECS9U6nz5kmrXKo6XVbtR48Ke3ZRxxgaWBu7IDtEe1quo8hiupwm6WFqxVlXaSf7IINGQ==", + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.11.3" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-webcontainers-wasm32": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.5.tgz", + "integrity": "sha512-hfhF/FmoQyTUkA0bIKFOtw536BQSeBMe6BF6QyWlrPxT754+TFLaZ7sKKTfvvM0yJgKgaYTwnFCIZ/GuDw5SUA==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/sharp-wasm32": "0.35.5" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.5.tgz", + "integrity": "sha512-X4t7g+7ZA5DKblCBEXGjUqqemj4vczING/5viFwAL8h4N3qYeyjwdCvRLHi4EdOUI+2Z7UFlp1VM+p/AuEtm6Q==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.5.tgz", + "integrity": "sha512-5Zm82LoBc43nhwNybZlG7Y1KO//Zhsn306fQl29ZOuStHLGTo3BWL83q3cznX0poxSAMuYL1On/BHBxkBeKr6A==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.5.tgz", + "integrity": "sha512-x76eH0vEiHlcMQu8Y8IenntaACtddpT6W0wmXtWrnKcnKI7ME5DdgqhAD6SEWOEl1v2zDvkZDhFA9KnURwpfqg==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz", + "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz", + "integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.29", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz", + "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@levischuck/tiny-cbor": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/@levischuck/tiny-cbor/-/tiny-cbor-0.2.11.tgz", + "integrity": "sha512-llBRm4dT4Z89aRsm6u2oEZ8tfwL/2l6BwpZ7JcyieouniDECM5AqNgr/y08zalEIvW3RSK4upYyybDcmjXqAow==", + "license": "MIT" + }, + "node_modules/@modelcontextprotocol/core": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/core/-/core-2.4.0.tgz", + "integrity": "sha512-z8QBKqdfXLPzkLQuG4xp8hrVEMm1PZ1IgxMN2sMbJiFiAKOWXnAzogXn2W4aqIBlP1ea6U5yiqVquvUNM1Cj6Q==", + "license": "Apache-2.0", + "dependencies": { + "zod": "^4.2.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@modelcontextprotocol/server": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/server/-/server-2.4.0.tgz", + "integrity": "sha512-rgR9yxSaAJ+bcsi/XroT/S2AfPiIWloXAnrdfYSCms9ivVju0ishu/wW0J21Ho6+ZOR1r7RyeRbjiRuF87ix5Q==", + "license": "Apache-2.0", + "dependencies": { + "@modelcontextprotocol/core": "2.4.0", + "zod": "^4.2.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@mongodb-js/saslprep": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.6.1.tgz", + "integrity": "sha512-D4CQtk9IwjCLAvCf1xvHEod3960q8XMUzfmdRU9qjUNlHnesGuTOvJcOTaPm9koZ1caUKA2aTbszZiAkcmSgmg==", + "license": "MIT", + "dependencies": { + "sparse-bitfield": "^3.0.3" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", + "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@tybys/wasm-util": "^0.10.0" + } + }, + "node_modules/@next/env": { + "version": "15.5.29", + "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.29.tgz", + "integrity": "sha512-9SrTiTsgORa+KfhlXXAGC1eSs847E+d2MQTPUX+n7R5XA8g/B6ZitfL6GoYHNMLwG3Hzs1wjDH0/ME5WrGMqBQ==", + "license": "MIT" + }, + "node_modules/@next/eslint-plugin-next": { + "version": "15.5.29", + "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-15.5.29.tgz", + "integrity": "sha512-d3dH0WVbXr+W/3f/TMU6fl7vHJBuok7ugMCDPfRnHhCLl8ppAtFbfXgVHpnLrUirHN4vE5GkTMnId3Fmxv9UCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-glob": "3.3.1" + } + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "15.5.29", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.29.tgz", + "integrity": "sha512-vozRDuM23T0faRR0O3oN1eRqjEqUue4Eljhkb9f7urQgzmyzve7j3atzTv3Z1AnjQ9IeVY0X3aS1/an56TOiTg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "15.5.29", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.29.tgz", + "integrity": "sha512-FzrcEzMZVcp/SqFXCW5SXyPil10bB9l5wpTBapEs9ZMqke3W+hyC5mNAdAa1divn+4SZ7LD5BtK67XvK7SucWQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "15.5.29", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.29.tgz", + "integrity": "sha512-FWbWE5P313Yy5062vgY9UspNz/t6Ve/s35X9v6YU7ilsbhqKGabPBPIWcMEPhyomeRdwfv19NAITkU2FFIhGwA==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "15.5.29", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.29.tgz", + "integrity": "sha512-xjTHrVisb9GeDSNFOwJI+4UJrKdwlpjz95q8sEvQh46yUp3+JikNcf26pode/HlHktSdJwcHx3iVMCjfPcnU+A==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "15.5.29", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.29.tgz", + "integrity": "sha512-5WqW7kHscT02wyF8TlbYv9+zx3KhnQeZwk+FkTCr1r7TuvMrqqOZ3RDkdDKl3O0M0Er3VuQEzoEIrj8VX5fjaw==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "15.5.29", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.29.tgz", + "integrity": "sha512-I0QJ1Be9vtdq5FABhNcSbkELllA91WnhG+m4BjJHCn1bXAIL8AuDABIKIJ7Bz84T/sIkZP6TvOEWJcKckrX3yQ==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "15.5.29", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.29.tgz", + "integrity": "sha512-q4fQFWtR7nH7ZDhU8RIvwNW0S/nHImaZkoOSw7rOXFgoP51HTesm8/8P/w3ACjc9GxqytXj0LZZesykK3arFBw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "15.5.29", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.29.tgz", + "integrity": "sha512-putWQ+CTEJunAPIPDPfb06Kc4emhYpdM5FoCDHSkC/ym7Dy2xOBMPgBjBbRkw2Hp5/550ACYxCmUtV4k4ui3Iw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@noble/hashes": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.4.0.tgz", + "integrity": "sha512-X5XaVWZIBCT7HHZGm5I7ZQXDwLG+bGXuSrMQAW+7Zvl87h1kmc1ZB1VSRJcpUfoUrGQp4Fkoxm5kZ+Ms+aW+eA==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nolyfill/is-core-module": { + "version": "1.0.39", + "resolved": "https://registry.npmjs.org/@nolyfill/is-core-module/-/is-core-module-1.0.39.tgz", + "integrity": "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.4.0" + } + }, + "node_modules/@panva/hkdf": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@panva/hkdf/-/hkdf-1.2.1.tgz", + "integrity": "sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/@peculiar/asn1-android": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-android/-/asn1-android-2.11.1.tgz", + "integrity": "sha512-AuUBRDNdWrgtnuJGXARa0Xdtq6OAD8GqezDQz6Qka7kyynMo9fzkcrtpBEHuX984uEB570BrhMZfJfkUDfpQfw==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.11.1", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@peculiar/asn1-cms": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-cms/-/asn1-cms-2.11.1.tgz", + "integrity": "sha512-Bk+oatE62dW0Tzzpfk6vdQxfGGEfvuGdWgmijnKjcv7HqZCJISs85z4zw2uDArrk+R1Oel1wts/BBl7HHrtAcA==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.11.1", + "@peculiar/asn1-x509": "^2.11.1", + "@peculiar/asn1-x509-attr": "^2.11.1", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@peculiar/asn1-csr": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-csr/-/asn1-csr-2.11.1.tgz", + "integrity": "sha512-KVfbXaKng8886/tOPG55t5K1XkTkGbR4amjIJzytXWobFU5nZgw/sKnSpa1hxP17nUSsrOF/7Mt9WUYeyLsi6Q==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.11.1", + "@peculiar/asn1-x509": "^2.11.1", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@peculiar/asn1-ecc": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-ecc/-/asn1-ecc-2.11.1.tgz", + "integrity": "sha512-7MSNji77fQa/oN3WAfwagMy8EoJFg50sW4hZ1dtr0zU1BSugit0I2DGpIdPhvkNplopQhz5bogxpn1077eaamA==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.11.1", + "@peculiar/asn1-x509": "^2.11.1", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@peculiar/asn1-pfx": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pfx/-/asn1-pfx-2.11.1.tgz", + "integrity": "sha512-2SN2z8qkc2hk/GgTYA8RN54wtsCDZShg9gI2Mq1vLhdj1adcv5fhTbiEmExZUaAMqlizxiEpULQDfHM5iYtn1g==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-cms": "^2.11.1", + "@peculiar/asn1-pkcs8": "^2.11.1", + "@peculiar/asn1-rsa": "^2.11.1", + "@peculiar/asn1-schema": "^2.11.1", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@peculiar/asn1-pkcs8": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs8/-/asn1-pkcs8-2.11.1.tgz", + "integrity": "sha512-HqsSrnpQghRp9SM9QSmR6fa4z/BNktnKq7928+oq4qF0VAnuHZ4cyePKvcxh9fP4taBCP/WHNItlVNsYaQm2qw==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.11.1", + "@peculiar/asn1-x509": "^2.11.1", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@peculiar/asn1-pkcs9": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs9/-/asn1-pkcs9-2.11.1.tgz", + "integrity": "sha512-OHFkTg8D2zYafILqFE4KPbqX8srpvd3diGDc2h2cduumawc765mOHGcuFMGvLWlL2bbCMWMmMSCtlXptsOuiPA==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-cms": "^2.11.1", + "@peculiar/asn1-pfx": "^2.11.1", + "@peculiar/asn1-pkcs8": "^2.11.1", + "@peculiar/asn1-schema": "^2.11.1", + "@peculiar/asn1-x509": "^2.11.1", + "@peculiar/asn1-x509-attr": "^2.11.1", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@peculiar/asn1-rsa": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-rsa/-/asn1-rsa-2.11.1.tgz", + "integrity": "sha512-6KwKGjulF489CpidZqkogNgLj6oGzgC4BL2FClM8PuJQ4t5NqT4q4a6A4vrSxBIBhMnL8hGYkLOpbVw0Vq38Tw==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.11.1", + "@peculiar/asn1-x509": "^2.11.1", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@peculiar/asn1-schema": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.11.1.tgz", + "integrity": "sha512-jaU9OaCcbZUdyXx9hMETZB9xoNmxTGjOGFqd3cOwKFkpKnyKd9WxF3uqj4PvX4cyOMP9bSdHg5YyML2qtx/d/g==", + "license": "MIT", + "dependencies": { + "@peculiar/utils": "^2.0.2", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@peculiar/asn1-x509": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509/-/asn1-x509-2.11.1.tgz", + "integrity": "sha512-SXU4/X6B3FquqQAN6ARI0bFDUV7+L1WlvPEPMKzEMkstd2D7UyM0r93fv241+OjdhqAKYGx5MvV3gzZMAp0APA==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.11.1", + "@peculiar/utils": "^2.0.2", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@peculiar/asn1-x509-attr": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509-attr/-/asn1-x509-attr-2.11.1.tgz", + "integrity": "sha512-n7RKFrbTzt8pGBlZ90kmKEn1qsLTTyw7lrvHMK0xsA42kQugPk/GXYpNbEcl7DkkdVsU/one5UX7KQPfxLj8vQ==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.11.1", + "@peculiar/asn1-x509": "^2.11.1", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@peculiar/utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@peculiar/utils/-/utils-2.0.3.tgz", + "integrity": "sha512-+oL3HPFRIZ1St2K50lWCXiioIgSoxzz7R1J3uF6neO2yl1sgmpgY6XXJH4BdpoDkMWznQTeYF6oWNDZLCdQ4eQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/x509": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/@peculiar/x509/-/x509-1.14.3.tgz", + "integrity": "sha512-C2Xj8FZ0uHWeCXXqX5B4/gVFQmtSkiuOolzAgutjTfseNOHT3pUjljDZsTSxXFGgio54bCzVFqmEOUrIVk8RDA==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-cms": "^2.6.0", + "@peculiar/asn1-csr": "^2.6.0", + "@peculiar/asn1-ecc": "^2.6.0", + "@peculiar/asn1-pkcs9": "^2.6.0", + "@peculiar/asn1-rsa": "^2.6.0", + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.0", + "pvtsutils": "^1.3.6", + "reflect-metadata": "^0.2.2", + "tslib": "^2.8.1", + "tsyringe": "^4.10.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@radix-ui/number": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.3.tgz", + "integrity": "sha512-Road2bidD0uu/1BGDOWNdPI06g0lIRy6IF9GZcIrDK2KGItfor8IQwQa+yM2ERgHM1MmHxaxpTzk0/Jp42lNfA==", + "license": "MIT" + }, + "node_modules/@radix-ui/primitive": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.2.0.tgz", + "integrity": "sha512-KABY+VMIRPJL+Vyzv/335FEhXXD/xDv8BMb1kl48/aXJZMH0LtHCTx5ga5RnxaEP3nZHtmTLf1P9vanmFX46bw==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-accessible-icon": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-accessible-icon/-/react-accessible-icon-1.2.0.tgz", + "integrity": "sha512-BB4+HHkFl3HipEAhm/mLhrpXV+2kRX7MvY4AaImUcQVeWQt5KhV1JEByNs8NGfJOcr95RcQGijt9+OrCo68xGA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-visually-hidden": "1.2.13" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-accordion": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.3.1.tgz", + "integrity": "sha512-lCZ6sbGS/7WO0j84yx4srIgOf0MRJ/sZ13RonADXxvKBym8xrxNCq5f6DibGcr38sIlwPk8/52uYdXeOPsbEDw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.2.0", + "@radix-ui/react-collapsible": "1.2.1", + "@radix-ui/react-collection": "1.2.0", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.3.0", + "@radix-ui/react-direction": "1.2.0", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-primitive": "2.2.0", + "@radix-ui/react-use-controllable-state": "1.3.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-alert-dialog": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.2.1.tgz", + "integrity": "sha512-MWC4N7RY+qsbNrFNo7yQ9ZbdoZEW+B5DtpWxJfUnVCqcQxxqqg3qis3RMPTDbCNaTIdHJSn5XDkl9ZdVWXknLg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.2.0", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.3.0", + "@radix-ui/react-dialog": "1.3.1", + "@radix-ui/react-primitive": "2.2.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-arrow": { + "version": "1.1.17", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.17.tgz", + "integrity": "sha512-67jjcVvxjXCCStnTW3Z+M7jmaFMuMR7M2JgpDzH4w7JSRrikmzhjfYgP/09ECb8n7whnsgh1ifJ+Ie96qEj+pQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.2.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-aspect-ratio": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-aspect-ratio/-/react-aspect-ratio-1.2.0.tgz", + "integrity": "sha512-gHEU+Lx8DlT/FYIjZQvvBJrFSwUngXTjh7FeL9QvJaNALDLez6pgDj2axCUihBjtPBy82LN0gNb25+sTcSXNmA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.2.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-avatar": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.3.1.tgz", + "integrity": "sha512-ssZOkfQNkFpYVmCPQlr978xSaU7moSrGXdwg4j2Xs2WlGl/1KsSZtfpK+WnLnx0Rwll1BAh6oUrAy/uR31SBvQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.2.0", + "@radix-ui/react-context": "1.3.0", + "@radix-ui/react-primitive": "2.2.0", + "@radix-ui/react-use-callback-ref": "1.1.5", + "@radix-ui/react-use-is-hydrated": "0.1.3", + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-checkbox": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.4.1.tgz", + "integrity": "sha512-KWtwhh+98ML3seZ9vO9yRY2OwaQG90dCiDAxjpSNY2AaZ3Hik3B0dsk6m0ILywo/no3A9/SozadyPMNweVT8yw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.2.0", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.3.0", + "@radix-ui/react-presence": "1.1.11", + "@radix-ui/react-primitive": "2.2.0", + "@radix-ui/react-use-controllable-state": "1.3.0", + "@radix-ui/react-use-size": "1.1.5" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collapsible": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.2.1.tgz", + "integrity": "sha512-fSzLb/qy1wuZSxQBcxpW0hNXYHDMWiIXDGyJGdA4+FSLknOWZsNRrTCUavxb4PI8b8lvHo/CysO6NVi5zLpo5g==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.2.0", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.3.0", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-presence": "1.1.11", + "@radix-ui/react-primitive": "2.2.0", + "@radix-ui/react-use-controllable-state": "1.3.0", + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.2.0.tgz", + "integrity": "sha512-+yiQzqcc7Sy7/5s0jrMCjpPRFB3538VNrXPWdTLZhE/SCfgEws4aI6lHWN/vvcnyhHndROWBzqmqB4QBrITZ9Q==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.3.0", + "@radix-ui/react-primitive": "2.2.0", + "@radix-ui/react-slot": "1.5.0", + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.5.tgz", + "integrity": "sha512-+48PbAAbq3didjJxa+OaWY2ZwgAKsNiRGyeHKszblZMQ+kcpd9pAaT11cMkGEie0vsOi3QdeTE6d5Fe3Gn61kA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.3.0.tgz", + "integrity": "sha512-v5AYEocMISkbJ+1fTXzZpiFv7NRr3D2WiYbLIKf+FkmJ8VkP6d503eV2n34MFussHXY1pcoq3yfJiQH77dc4Mg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context-menu": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context-menu/-/react-context-menu-2.4.1.tgz", + "integrity": "sha512-Pddn3Z8lio3/ChxyXaEP3jHTTo1LTHCWwF8+mG1chQlWijP+ttVHBKwezrQTiV6Mj670ojvumqvuGh+mdv33iQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.2.0", + "@radix-ui/react-context": "1.3.0", + "@radix-ui/react-menu": "2.2.1", + "@radix-ui/react-primitive": "2.2.0", + "@radix-ui/react-use-controllable-state": "1.3.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.3.1.tgz", + "integrity": "sha512-+0E4qvVTwjAWra9sEhJ+9BTsvHE00zkUuNw1VGh7S+Gc13dfYk+S1PVQdchWDj+XYHhAai7Z7IpzXVDCGsDplg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.2.0", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.3.0", + "@radix-ui/react-dismissable-layer": "1.2.1", + "@radix-ui/react-focus-guards": "1.2.0", + "@radix-ui/react-focus-scope": "1.3.1", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-portal": "1.2.0", + "@radix-ui/react-presence": "1.1.11", + "@radix-ui/react-primitive": "2.2.0", + "@radix-ui/react-slot": "1.5.0", + "@radix-ui/react-use-controllable-state": "1.3.0", + "@radix-ui/react-use-layout-effect": "1.1.4", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.7.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-direction": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.2.0.tgz", + "integrity": "sha512-FtRA52a/r+LKStFT+neLUm+8vSe7wbzkbQGGeU8ixFiwC8NU14Evoq9ATECqL3eGbcUzJR4fyfA4KkK/q6dfqg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.2.1.tgz", + "integrity": "sha512-KIfDcAsx+oVylFZ4xryVKusJUrLLhTQmfnJW4kwk4Km5myvhHCrj04hnWWUC166+xD4DHdH8tQGylwYX1h6+Pg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.2.0", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-primitive": "2.2.0", + "@radix-ui/react-use-callback-ref": "1.1.5", + "@radix-ui/react-use-effect-event": "0.0.5" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dropdown-menu": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.2.1.tgz", + "integrity": "sha512-XsHcmYG2AcqnvTqrOXXWcMrhquEIlqucizuDH1MV/qMRW+DndpBCMvNeTRXOxqmG1H5j89M/+I0/34Zdd+zDcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.2.0", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.3.0", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-menu": "2.2.1", + "@radix-ui/react-primitive": "2.2.0", + "@radix-ui/react-use-controllable-state": "1.3.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.2.0.tgz", + "integrity": "sha512-MPm5R0iRGUDrDnPNxmLBkv+X7pN4XKzt+MbWJEuJQsH3NFH4omrbpXpucyYCCKTSt+na/ZxGhEWQrWTZ0285cA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.3.1.tgz", + "integrity": "sha512-ugeMhbXt4y5S2T1GnIqbWDKEN962PDwnX1fN0eKOOgf5f63Dh2mn8F1FUySlgW0iF6GiVsRF32DzuDIWTUyQJg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-primitive": "2.2.0", + "@radix-ui/react-use-callback-ref": "1.1.5" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-form": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-form/-/react-form-0.3.0.tgz", + "integrity": "sha512-KnmrCM27m7Jo1R8S6cFDPrEeq20Jfn5dqde0SZrJr2lVXlj6/QZSUCyFoYh1d8ti0uUrMNK5Lx6dUDuINT+f4Q==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.2.0", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.3.0", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-label": "2.1.17", + "@radix-ui/react-primitive": "2.2.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-hover-card": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-hover-card/-/react-hover-card-1.2.1.tgz", + "integrity": "sha512-4QooHxgmpCKRjj1Pft+hETKidYcrbPKcVwMGtk2Oa5DcWY2IH1fHuQt9LG6q5hUQBBV2MhQxSvtx6B/C1xfiTw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.2.0", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.3.0", + "@radix-ui/react-dismissable-layer": "1.2.1", + "@radix-ui/react-focus-scope": "1.3.1", + "@radix-ui/react-popper": "1.4.0", + "@radix-ui/react-portal": "1.2.0", + "@radix-ui/react-presence": "1.1.11", + "@radix-ui/react-primitive": "2.2.0", + "@radix-ui/react-use-controllable-state": "1.3.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.4.tgz", + "integrity": "sha512-TMQp2llA+RYn7JcjnrMnz7wN4pcVttPZnRZo52PLQsoLVKzNlVwUeHmfePgTgRluXFvlD3GD5g5MOVVTJCO0qA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-label": { + "version": "2.1.17", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.17.tgz", + "integrity": "sha512-xmj7MN/drx1oUAXkfiFIvyDmGNUIN/omGobvA5oAoNqu9zO2MkISYGZOkBcqSzs/UZXXwV+/3ACRE7q7mGcmkA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.2.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.2.1.tgz", + "integrity": "sha512-G34BsU90SBj5foYpz8pQqq/Q0Dlfyyl6KitWEfRwXC0QvfwhWNAwS49DWP2+rRJ0iwv20Z2Qx7Sk+T03bGo5JA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.2.0", + "@radix-ui/react-collection": "1.2.0", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.3.0", + "@radix-ui/react-direction": "1.2.0", + "@radix-ui/react-dismissable-layer": "1.2.1", + "@radix-ui/react-focus-guards": "1.2.0", + "@radix-ui/react-focus-scope": "1.3.1", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-popper": "1.4.0", + "@radix-ui/react-portal": "1.2.0", + "@radix-ui/react-presence": "1.1.11", + "@radix-ui/react-primitive": "2.2.0", + "@radix-ui/react-roving-focus": "1.2.1", + "@radix-ui/react-slot": "1.5.0", + "@radix-ui/react-use-callback-ref": "1.1.5", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.7.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menubar": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menubar/-/react-menubar-1.2.1.tgz", + "integrity": "sha512-bRTYtS9JITV2dUYqYZb7+N5SooQBkZrNRejfmyrSKe3Y57aXhKgx//kBUP0kY7pEpsWR/U1AHsAJRzjwCrK3PA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.2.0", + "@radix-ui/react-collection": "1.2.0", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.3.0", + "@radix-ui/react-direction": "1.2.0", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-menu": "2.2.1", + "@radix-ui/react-primitive": "2.2.0", + "@radix-ui/react-roving-focus": "1.2.1", + "@radix-ui/react-use-controllable-state": "1.3.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-navigation-menu": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.4.1.tgz", + "integrity": "sha512-XPPZjkeyCSMNNvt0FyDv/PUauG7Ow4GwHQ/XZSwW36F/TS7rR4HFTsXH8ewLnXTOLn3x96/pmTPNQIVoTX/86g==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.2.0", + "@radix-ui/react-collection": "1.2.0", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.3.0", + "@radix-ui/react-direction": "1.2.0", + "@radix-ui/react-dismissable-layer": "1.2.1", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-presence": "1.1.11", + "@radix-ui/react-primitive": "2.2.0", + "@radix-ui/react-slot": "1.5.0", + "@radix-ui/react-use-callback-ref": "1.1.5", + "@radix-ui/react-use-controllable-state": "1.3.0", + "@radix-ui/react-use-layout-effect": "1.1.4", + "@radix-ui/react-use-previous": "1.1.4", + "@radix-ui/react-visually-hidden": "1.2.13" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-one-time-password-field": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-one-time-password-field/-/react-one-time-password-field-0.2.1.tgz", + "integrity": "sha512-KtHdcgnaAK6rohQ/MMJoGYcbgSRJsIuGBdHrFeTB1/9SaKlDs14oSOniW6BL6M43HqvgCO3u4WuVdFLkuc6sqw==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.3", + "@radix-ui/primitive": "1.2.0", + "@radix-ui/react-collection": "1.2.0", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.3.0", + "@radix-ui/react-direction": "1.2.0", + "@radix-ui/react-primitive": "2.2.0", + "@radix-ui/react-roving-focus": "1.2.1", + "@radix-ui/react-use-controllable-state": "1.3.0", + "@radix-ui/react-use-effect-event": "0.0.5", + "@radix-ui/react-use-is-hydrated": "0.1.3", + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-password-toggle-field": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-password-toggle-field/-/react-password-toggle-field-0.2.1.tgz", + "integrity": "sha512-vu4+ZoJAaFjdI7l1lDSZRs+4dEv83Jmu7lyzajPrAeQzX74y84n0rUNWbWSgN9XB5NE8IY86Z02kGH6oRrzR+w==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.2.0", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.3.0", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-primitive": "2.2.0", + "@radix-ui/react-use-controllable-state": "1.3.0", + "@radix-ui/react-use-effect-event": "0.0.5", + "@radix-ui/react-use-is-hydrated": "0.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.3.1.tgz", + "integrity": "sha512-ZNxvXRx5W2Wos4fXCXb5lpdqN0Z5chf+mStOUOTr4ik7d+62MRvoe9rzCA/eGNg36lxXqSrKEKmwuZEcUrIstw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.2.0", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.3.0", + "@radix-ui/react-dismissable-layer": "1.2.1", + "@radix-ui/react-focus-guards": "1.2.0", + "@radix-ui/react-focus-scope": "1.3.1", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-popper": "1.4.0", + "@radix-ui/react-portal": "1.2.0", + "@radix-ui/react-presence": "1.1.11", + "@radix-ui/react-primitive": "2.2.0", + "@radix-ui/react-slot": "1.5.0", + "@radix-ui/react-use-controllable-state": "1.3.0", + "@radix-ui/react-use-layout-effect": "1.1.4", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.7.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.4.0.tgz", + "integrity": "sha512-yCWRvLeGv9Pz3+efwHqGPp70mZ56r3kfIgQVF56Oj2aAa4uP/Tb+IE59wNcc+758rctqbCKx4LbK8II/r7Bn0Q==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.17", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.3.0", + "@radix-ui/react-primitive": "2.2.0", + "@radix-ui/react-use-callback-ref": "1.1.5", + "@radix-ui/react-use-layout-effect": "1.1.4", + "@radix-ui/react-use-rect": "1.1.4", + "@radix-ui/react-use-size": "1.1.5", + "@radix-ui/rect": "1.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.2.0.tgz", + "integrity": "sha512-StX7xXnaRU41VvFtMm9zlhVGz8XF6GmWWTXftkQaY+tqVkbs/R9UmAp5mLzHHapk5hM9RMZQvO/3usyIP/g42g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.2.0", + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.11.tgz", + "integrity": "sha512-Z7/atlkab36Z8hm+V8zYc79r0W+tiOB1o0tun1By94JyadkekLQmtEAnBSP9XKXBNQsStoStS4+gX10JpS5akA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.2.0.tgz", + "integrity": "sha512-HCTSPMNqv55Qk0IX4geJbpyXazmqulcimwuyjxTkKi29v+QZbG2Zho9BnbBq4YS2PF+KlNb2CXoLz66uUmblkg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.5.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-progress": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.2.1.tgz", + "integrity": "sha512-yQBiM6J3YM8Ums1orkEFpbznK47geKxa0igikzcaS1xHgdba3DdAQBsGbCTt5BVfR8zhEvgxQ0poGcINaD3lvw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-context": "1.3.0", + "@radix-ui/react-primitive": "2.2.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-radio-group": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.5.1.tgz", + "integrity": "sha512-QbyVT1ddnjqB9H+CUYKkNWdU9fIK3QHCYtjf7mS6ekSqiuXkWtWn5UZW9mhA8qtl3vrVyIsmR9gePstAtKrqkw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.2.0", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.3.0", + "@radix-ui/react-direction": "1.2.0", + "@radix-ui/react-presence": "1.1.11", + "@radix-ui/react-primitive": "2.2.0", + "@radix-ui/react-roving-focus": "1.2.1", + "@radix-ui/react-use-controllable-state": "1.3.0", + "@radix-ui/react-use-size": "1.1.5" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.2.1.tgz", + "integrity": "sha512-UwraORzVUeEPMclhO1p0vz0qI+TbdDLO9i+FiOqhJyHKwdpCRCck9yVEscUnAMIzCiuNoQa8D4A9bIpOPXGt6Q==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.2.0", + "@radix-ui/react-collection": "1.2.0", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.3.0", + "@radix-ui/react-direction": "1.2.0", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-primitive": "2.2.0", + "@radix-ui/react-use-callback-ref": "1.1.5", + "@radix-ui/react-use-controllable-state": "1.3.0", + "@radix-ui/react-use-is-hydrated": "0.1.3", + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-scroll-area": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.4.0.tgz", + "integrity": "sha512-CFrLqHNejWq5FhKAcj9V/WPGRsol76HBQto81NdOBVFEOqIEkJkKIJt0Z824A/9hHLCtoiR/Kw3euRgzXP2pxg==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.3", + "@radix-ui/primitive": "1.2.0", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.3.0", + "@radix-ui/react-direction": "1.2.0", + "@radix-ui/react-presence": "1.1.11", + "@radix-ui/react-primitive": "2.2.0", + "@radix-ui/react-slot": "1.5.0", + "@radix-ui/react-use-callback-ref": "1.1.5", + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.4.1.tgz", + "integrity": "sha512-888ZwW0Md6GyptuqYABRX9nLKgoYfcooCyBBvVaX5Jp3e+8Jp2i4Zo/Uy50Hm+Ysn2JfloQXstW4U36w8teVtg==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.3", + "@radix-ui/primitive": "1.2.0", + "@radix-ui/react-collection": "1.2.0", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.3.0", + "@radix-ui/react-direction": "1.2.0", + "@radix-ui/react-dismissable-layer": "1.2.1", + "@radix-ui/react-focus-guards": "1.2.0", + "@radix-ui/react-focus-scope": "1.3.1", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-popper": "1.4.0", + "@radix-ui/react-portal": "1.2.0", + "@radix-ui/react-presence": "1.1.11", + "@radix-ui/react-primitive": "2.2.0", + "@radix-ui/react-slot": "1.5.0", + "@radix-ui/react-use-callback-ref": "1.1.5", + "@radix-ui/react-use-controllable-state": "1.3.0", + "@radix-ui/react-use-layout-effect": "1.1.4", + "@radix-ui/react-visually-hidden": "1.2.13", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.7.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-separator": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.2.0.tgz", + "integrity": "sha512-1jUAap0wZEqVP05Ou7kJgRxe/W+Qftng+rUNWGa0qkKbv9r5pWTG3E7a3ELKo2eJoHKMECnzuYrZpSyO1YgcYQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.2.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slider": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.6.1.tgz", + "integrity": "sha512-IRNRR90moPqxfwCJ0XbMJIxo70W0/1ZExT/sNCxEHMaPx0SF9HoQrcPEC75+H28cFKa0b+KH63anfLYJmwKacw==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.3", + "@radix-ui/primitive": "1.2.0", + "@radix-ui/react-collection": "1.2.0", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.3.0", + "@radix-ui/react-direction": "1.2.0", + "@radix-ui/react-primitive": "2.2.0", + "@radix-ui/react-use-controllable-state": "1.3.0", + "@radix-ui/react-use-layout-effect": "1.1.4", + "@radix-ui/react-use-size": "1.1.5" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.5.0.tgz", + "integrity": "sha512-tyYd9ddPXAysAbdbD9eYxgwpCSv6f3PncqVSNKQFSR5lcJVLz3cb/pANDoYrLiHBpXv7skortyUgGUWsYuPPFQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.5" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-switch": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.4.1.tgz", + "integrity": "sha512-NpBGwc40rY02ZmSvuyeKk0Bvc8ZsNeTMi4qHvJ+ZH4EiYCv0zIXEgdCW4bVdzgIUeP9xxbLJSxKOUfqITYNmkA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.2.0", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.3.0", + "@radix-ui/react-primitive": "2.2.0", + "@radix-ui/react-use-controllable-state": "1.3.0", + "@radix-ui/react-use-size": "1.1.5" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.2.1.tgz", + "integrity": "sha512-hRoJlode+hg5ZNlR+3tkfkUliIybdTpD5lkAvHQ9yaXseGf7XVO9Ib2GPEIOuaJJbo7YWYbNpDDRfo8Dhf6/xA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.2.0", + "@radix-ui/react-context": "1.3.0", + "@radix-ui/react-direction": "1.2.0", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-presence": "1.1.11", + "@radix-ui/react-primitive": "2.2.0", + "@radix-ui/react-roving-focus": "1.2.1", + "@radix-ui/react-use-controllable-state": "1.3.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toast": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.3.1.tgz", + "integrity": "sha512-iQvL0J7XK4HtJNGkkWMyYrboNseI7vtotFeUkIno8YNxVA9oM7B6//fh0Mc6TSKI96+zioLWi98lQLso88yJmQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.2.0", + "@radix-ui/react-collection": "1.2.0", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.3.0", + "@radix-ui/react-dismissable-layer": "1.2.1", + "@radix-ui/react-portal": "1.2.0", + "@radix-ui/react-presence": "1.1.11", + "@radix-ui/react-primitive": "2.2.0", + "@radix-ui/react-use-callback-ref": "1.1.5", + "@radix-ui/react-use-controllable-state": "1.3.0", + "@radix-ui/react-use-layout-effect": "1.1.4", + "@radix-ui/react-visually-hidden": "1.2.13" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.2.1.tgz", + "integrity": "sha512-+3MUq+WyiBplXYqQ0EIYlq8R4sLQ0T5NbOK7DMdDLCKO93pe2uE0DUJTWWfTBprpJJXxfoF3eMW0xpakVZrYsg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.2.0", + "@radix-ui/react-primitive": "2.2.0", + "@radix-ui/react-use-controllable-state": "1.3.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle-group": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle-group/-/react-toggle-group-1.2.1.tgz", + "integrity": "sha512-O8Ldx5ao4p4KoW+DO8/PmkOe5mdKNi3F2SseqjVj88wvDGDHe60cUmO0Zm/uHsKrvNgQGV8Efpuh95Tht/h4vw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.2.0", + "@radix-ui/react-context": "1.3.0", + "@radix-ui/react-direction": "1.2.0", + "@radix-ui/react-primitive": "2.2.0", + "@radix-ui/react-roving-focus": "1.2.1", + "@radix-ui/react-toggle": "1.2.1", + "@radix-ui/react-use-controllable-state": "1.3.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } } }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, + "node_modules/@radix-ui/react-toolbar": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toolbar/-/react-toolbar-1.2.1.tgz", + "integrity": "sha512-Cm8m7JTEeUdbfA6T9xgyu8HfOpyvSx8pB/wj0DlEuWaJe9FilIs4Az9wci/lU9RbthV/UkltszMw5WnnD+m/Ug==", "license": "MIT", - "engines": { - "node": ">=6.0.0" + "dependencies": { + "@radix-ui/primitive": "1.2.0", + "@radix-ui/react-context": "1.3.0", + "@radix-ui/react-direction": "1.2.0", + "@radix-ui/react-primitive": "2.2.0", + "@radix-ui/react-roving-focus": "1.2.1", + "@radix-ui/react-separator": "1.2.0", + "@radix-ui/react-toggle-group": "1.2.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } } }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz", - "integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==", - "dev": true, - "license": "MIT" + "node_modules/@radix-ui/react-tooltip": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.4.1.tgz", + "integrity": "sha512-NxLFGSFtZ78zVfcoOhTwR1xwqo5qx1Jh8Pw6FMZ9McCaqfRzz7/gIRahs2Oy5XlPgQ+8QkUcKbN5B0U44J/kCg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.2.0", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.3.0", + "@radix-ui/react-dismissable-layer": "1.2.1", + "@radix-ui/react-focus-scope": "1.3.1", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-popper": "1.4.0", + "@radix-ui/react-portal": "1.2.0", + "@radix-ui/react-presence": "1.1.11", + "@radix-ui/react-primitive": "2.2.0", + "@radix-ui/react-slot": "1.5.0", + "@radix-ui/react-use-controllable-state": "1.3.0", + "@radix-ui/react-use-layout-effect": "1.1.4", + "@radix-ui/react-visually-hidden": "1.2.13" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.29", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz", - "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", - "dev": true, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.5.tgz", + "integrity": "sha512-Y5GBf4NKeADoXI1geiMlMuMoURT3k9MizzzKhGb/m55H94aGigZ01PIpkvsO6lML13BA6XYAXSJ6gza7vTn5nw==", "license": "MIT", "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, - "node_modules/@mongodb-js/saslprep": { + "node_modules/@radix-ui/react-use-controllable-state": { "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.3.0.tgz", - "integrity": "sha512-zlayKCsIjYb7/IdfqxorK5+xUMyi4vOKcFy10wKJYc63NSdKI8mNME+uJqfatkPmOSMMUiojrL58IePKBm3gvQ==", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.3.0.tgz", + "integrity": "sha512-TAcBI7bw2ljYCbv4+zuTSd6zICu8pRt6SDN2vJ/Z06K2MXh6I9WYFe/A9ovlFDtR5HVHGm5iDxDoABm/++dbKg==", "license": "MIT", "dependencies": { - "sparse-bitfield": "^3.0.3" + "@radix-ui/primitive": "1.2.0", + "@radix-ui/react-use-effect-event": "0.0.5", + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, - "node_modules/@napi-rs/wasm-runtime": { - "version": "0.2.12", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", - "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", - "dev": true, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.5.tgz", + "integrity": "sha512-7cshFL8HGS/7HEiHH+9kL9HBwp2sa9yX18Knwek6KYWmXwM7pegMgta2AXMQKI+rq3JnfSj9x8wYqFMTdG1Jgg==", "license": "MIT", - "optional": true, "dependencies": { - "@emnapi/core": "^1.4.3", - "@emnapi/runtime": "^1.4.3", - "@tybys/wasm-util": "^0.10.0" + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, - "node_modules/@next/env": { - "version": "15.4.4", - "resolved": "https://registry.npmjs.org/@next/env/-/env-15.4.4.tgz", - "integrity": "sha512-SJKOOkULKENyHSYXE5+KiFU6itcIb6wSBjgM92meK0HVKpo94dNOLZVdLLuS7/BxImROkGoPsjR4EnuDucqiiA==", - "license": "MIT" - }, - "node_modules/@next/eslint-plugin-next": { - "version": "15.3.2", - "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-15.3.2.tgz", - "integrity": "sha512-ijVRTXBgnHT33aWnDtmlG+LJD+5vhc9AKTJPquGG5NKXjpKNjc62woIhFtrAcWdBobt8kqjCoaJ0q6sDQoX7aQ==", - "dev": true, + "node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.6.tgz", + "integrity": "sha512-9cRullnQRv+kqnRmFlmh0WC3kAPuGmHiQ9VwsXdrYk//sBhgCKEttcpPEPgIQbyx6TGUmooVEHYTyohwz/lSJg==", "license": "MIT", "dependencies": { - "fast-glob": "3.3.1" + "@radix-ui/react-use-callback-ref": "1.1.5" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, - "node_modules/@next/swc-darwin-arm64": { - "version": "15.4.4", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.4.4.tgz", - "integrity": "sha512-eVG55dnGwfUuG+TtnUCt+mEJ+8TGgul6nHEvdb8HEH7dmJIFYOCApAaFrIrxwtEq2Cdf+0m5sG1Np8cNpw9EAw==", - "cpu": [ - "arm64" - ], + "node_modules/@radix-ui/react-use-is-hydrated": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.3.tgz", + "integrity": "sha512-umO/aJ+82CpOnhDZUTbILCQf7kU/g0iv+oGs/Q8jw7IkhWBzaEP4sA268PhFAJTFetbwp3ICc6ktpI4TqtxcIw==", "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, - "node_modules/@next/swc-darwin-x64": { - "version": "15.4.4", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.4.4.tgz", - "integrity": "sha512-zqG+/8apsu49CltEj4NAmCGZvHcZbOOOsNoTVeIXphYWIbE4l6A/vuQHyqll0flU2o3dmYCXsBW5FmbrGDgljQ==", - "cpu": [ - "x64" - ], + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.4.tgz", + "integrity": "sha512-K20DkRkUwDnxEYMBPcg3Y6voLkEy5p5QQmszZgLngKKiC7dzBR/aEuK3w1qlx2JWDUNH6FluahYdgR3BP+QbYw==", "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, - "node_modules/@next/swc-linux-arm64-gnu": { - "version": "15.4.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.4.4.tgz", - "integrity": "sha512-LRD4l2lq4R+2QCHBQVC0wjxxkLlALGJCwigaJ5FSRSqnje+MRKHljQNZgDCaKUZQzO/TXxlmUdkZP/X3KNGZaw==", - "cpu": [ - "arm64" - ], + "node_modules/@radix-ui/react-use-previous": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.4.tgz", + "integrity": "sha512-XoSLhbRbqxFtgJoi2fNHA3C6pDlY34x508vUpUGoFZfvePfHXHbE1lC4FYFMnJWgiCRroSTw6fOsXQoVS9RwZg==", "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, - "node_modules/@next/swc-linux-arm64-musl": { - "version": "15.4.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.4.4.tgz", - "integrity": "sha512-LsGUCTvuZ0690fFWerA4lnQvjkYg9gHo12A3wiPUR4kCxbx/d+SlwmonuTH2SWZI+RVGA9VL3N0S03WTYv6bYg==", - "cpu": [ - "arm64" - ], + "node_modules/@radix-ui/react-use-rect": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.4.tgz", + "integrity": "sha512-cSOCh6JlkmfjLyNcLiu2nB4v+nm+dkZ+Q5KHWk/soo4U7ZLiEQFKHK9/YmtBHjfCEaU43IBKQOc4/uJmCaiCTQ==", "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" + "dependencies": { + "@radix-ui/rect": "1.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, - "node_modules/@next/swc-linux-x64-gnu": { - "version": "15.4.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.4.4.tgz", - "integrity": "sha512-aOy5yNRpLL3wNiJVkFYl6w22hdREERNjvegE6vvtix8LHRdsTHhWTpgvcYdCK7AIDCQW5ATmzr9XkPHvSoAnvg==", - "cpu": [ - "x64" - ], + "node_modules/@radix-ui/react-use-size": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.5.tgz", + "integrity": "sha512-V1McDQkMGzgivwqdu7u96APs7oi0Jvj9LTzgq3WWrDZqxWdAqU9yZnZQqk5OizGFR8badUQwZsrlao7MOlWYOA==", "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, - "node_modules/@next/swc-linux-x64-musl": { - "version": "15.4.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.4.4.tgz", - "integrity": "sha512-FL7OAn4UkR8hKQRGBmlHiHinzOb07tsfARdGh7v0Z0jEJ3sz8/7L5bR23ble9E6DZMabSStqlATHlSxv1fuzAg==", - "cpu": [ - "x64" - ], + "node_modules/@radix-ui/react-visually-hidden": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.13.tgz", + "integrity": "sha512-feijiN1oPk4rck4u9Dm9tsU4mT6Wp9oeEEhaz1WUEs1VuqLf8xzsFG+Qk+uWq6TnMH9O+WRECRg0NftyP9E4Fw==", "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" + "dependencies": { + "@radix-ui/react-primitive": "2.2.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } } }, - "node_modules/@next/swc-win32-arm64-msvc": { - "version": "15.4.4", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.4.4.tgz", - "integrity": "sha512-eEdNW/TXwjYhOulQh0pffTMMItWVwKCQpbziSBmgBNFZIIRn2GTXrhrewevs8wP8KXWYMx8Z+mNU0X+AfvtrRg==", - "cpu": [ - "arm64" - ], + "node_modules/@radix-ui/rect": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.3.tgz", + "integrity": "sha512-JtyZR+mqgBibTo8xea3B6ZRmzZiM/YeVBtUkas6zMuXjAlfIFIW2FgqeM9eLyvEaYX66vr6DJMK+4U6LV0KhNw==", + "license": "MIT" + }, + "node_modules/@rtsao/scc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rushstack/eslint-patch": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.12.0.tgz", + "integrity": "sha512-5EwMtOqvJMMa3HbmxLlF74e+3/HhwBTMcvt3nqVJgGCozO6hzIPOBlwm8mGVNR9SN2IJpxSnlxczyDjcn7qIyw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sec-ant/readable-stream": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", + "integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==", + "license": "MIT" + }, + "node_modules/@shadcn/registry": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@shadcn/registry/-/registry-0.2.1.tgz", + "integrity": "sha512-wMvZ3DGdnhCwtpeFbwKybSGfCcP52bZX77jMKgZvupmirgyA5KRLlkeQXkI2RGHIaD8TX2iWe2wcIEjjqzg2FQ==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/parser": "^7.28.0", + "@babel/plugin-transform-typescript": "^7.28.0", + "@babel/preset-typescript": "^7.27.1", + "@dotenvx/dotenvx": "^1.48.4", + "cn": "^0.2.4", + "code-block-writer": "^13.0.3", + "deepmerge": "^4.3.1", + "execa": "^9.6.0", + "fast-glob": "^3.3.3", + "fs-extra": "^11.3.1", + "fuzzysort": "^3.1.0", + "kleur": "^4.1.5", + "ora": "^8.2.0", + "postcss": "^8.5.23", + "prompts": "^2.4.2", + "recast": "^0.23.11", + "socks": "^2.8.8", + "stringify-object": "^5.0.0", + "tsconfig-paths": "^4.2.0", + "type-fest": "^4.41.0", + "undici": "^7.30.1", + "zod": "^4.2.0" + }, + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/@shadcn/registry/node_modules/cn": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/cn/-/cn-0.2.6.tgz", + "integrity": "sha512-+i4L0zUGgRcEnhsxueVrP7iBGxBx5iD0WOTYg1MwFEu2ZyCmH5Ov2V1cul2Ht5UchRQQgftCf4be/RxspuW6QQ==", "license": "MIT", - "optional": true, - "os": [ - "win32" - ], + "bin": { + "cn": "bin/cn.mjs" + }, "engines": { - "node": ">= 10" + "node": ">=20" } }, - "node_modules/@next/swc-win32-x64-msvc": { - "version": "15.4.4", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.4.4.tgz", - "integrity": "sha512-SE5pYNbn/xZKMy1RE3pAs+4xD32OI4rY6mzJa4XUkp/ItZY+OMjIgilskmErt8ls/fVJ+Ihopi2QIeW6O3TrMw==", - "cpu": [ - "x64" - ], + "node_modules/@shadcn/registry/node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", "license": "MIT", - "optional": true, - "os": [ - "win32" - ], + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, "engines": { - "node": ">= 10" + "node": ">=8.6.0" } }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "license": "MIT", + "node_modules/@shadcn/registry/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "license": "ISC", "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" + "is-glob": "^4.0.1" }, "engines": { - "node": ">= 8" + "node": ">= 6" } }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, + "node_modules/@shadcn/registry/node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, "engines": { - "node": ">= 8" + "node": ">=6" } }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, + "node_modules/@shadcn/registry/node_modules/tsconfig-paths": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", + "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", "license": "MIT", "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" + "json5": "^2.2.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" }, "engines": { - "node": ">= 8" + "node": ">=6" } }, - "node_modules/@nolyfill/is-core-module": { - "version": "1.0.39", - "resolved": "https://registry.npmjs.org/@nolyfill/is-core-module/-/is-core-module-1.0.39.tgz", - "integrity": "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==", - "dev": true, + "node_modules/@shadcn/registry/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@simplewebauthn/browser": { + "version": "13.3.0", + "resolved": "https://registry.npmjs.org/@simplewebauthn/browser/-/browser-13.3.0.tgz", + "integrity": "sha512-BE/UWv6FOToAdVk0EokzkqQQDOWtNydYlY6+OrmiZ5SCNmb41VehttboTetUM3T/fr6EAFYVXjz4My2wg230rQ==", + "license": "MIT" + }, + "node_modules/@simplewebauthn/server": { + "version": "13.3.3", + "resolved": "https://registry.npmjs.org/@simplewebauthn/server/-/server-13.3.3.tgz", + "integrity": "sha512-LelX/lcy5cjc15A86i/aNxHhB5eU7dd20QsbP0VLAf9e38+SLlsnqCCyecx3xqfGofhmX05h1J9fKRYWxw+luA==", "license": "MIT", + "dependencies": { + "@hexagon/base64": "^1.1.27", + "@levischuck/tiny-cbor": "^0.2.2", + "@peculiar/asn1-android": "^2.6.0", + "@peculiar/asn1-ecc": "^2.6.1", + "@peculiar/asn1-rsa": "^2.6.1", + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.1", + "@peculiar/x509": "^1.14.3" + }, "engines": { - "node": ">=12.4.0" + "node": ">=20.0.0" } }, - "node_modules/@panva/hkdf": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@panva/hkdf/-/hkdf-1.2.1.tgz", - "integrity": "sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw==", + "node_modules/@sindresorhus/merge-streams": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz", + "integrity": "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==", "license": "MIT", + "engines": { + "node": ">=18" + }, "funding": { - "url": "https://github.com/sponsors/panva" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@rtsao/scc": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", - "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", - "dev": true, - "license": "MIT" - }, - "node_modules/@rushstack/eslint-patch": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.12.0.tgz", - "integrity": "sha512-5EwMtOqvJMMa3HbmxLlF74e+3/HhwBTMcvt3nqVJgGCozO6hzIPOBlwm8mGVNR9SN2IJpxSnlxczyDjcn7qIyw==", - "dev": true, - "license": "MIT" - }, "node_modules/@swc/helpers": { "version": "0.5.15", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", @@ -1199,6 +4105,81 @@ "tailwindcss": "4.1.11" } }, + "node_modules/@ts-morph/common": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.27.0.tgz", + "integrity": "sha512-Wf29UqxWDpc+i61k3oIOzcUfQt79PIT9y/MWfAGlrkjg6lBC1hwDECLXPVJAhWjiGbfBCxZd65F/LIZF3+jeJQ==", + "license": "MIT", + "dependencies": { + "fast-glob": "^3.3.3", + "minimatch": "^10.0.1", + "path-browserify": "^1.0.1" + } + }, + "node_modules/@ts-morph/common/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@ts-morph/common/node_modules/brace-expansion": { + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@ts-morph/common/node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/@ts-morph/common/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@ts-morph/common/node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/@tybys/wasm-util": { "version": "0.10.0", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.0.tgz", @@ -1217,6 +4198,20 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/estree": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.10.tgz", + "integrity": "sha512-lTyOMnOcmRBzdCv6/4zUZCrEhV0xrqQiUZBZtVcbmLHBXKLnN8q89v/wKRF/Q0HbwVsqbIG3o5HDoFl9sduM+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/json5": { "version": "0.0.29", "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", @@ -1234,11 +4229,21 @@ "undici-types": "~6.21.0" } }, + "node_modules/@types/qrcode": { + "version": "1.5.6", + "resolved": "https://registry.npmjs.org/@types/qrcode/-/qrcode-1.5.6.tgz", + "integrity": "sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/react": { "version": "19.1.9", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.9.tgz", "integrity": "sha512-WmdoynAX8Stew/36uTSVMcLJJ1KRh6L3IZRx1PZ7qJtBqT3dYTgyDTx8H1qoRghErydW7xw9mSJ3wS//tCRpFA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "csstype": "^3.0.2" @@ -1248,12 +4253,18 @@ "version": "19.1.7", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.7.tgz", "integrity": "sha512-i5ZzwYpqjmrKenzkoLM2Ibzt6mAsM7pxB6BCIouEVVmgiqaMj1TjaK7hnA36hbW5aZv20kx7Lw6hWzPWg0Rurw==", - "dev": true, + "devOptional": true, "license": "MIT", "peerDependencies": { "@types/react": "^19.0.0" } }, + "node_modules/@types/validate-npm-package-name": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/validate-npm-package-name/-/validate-npm-package-name-4.0.2.tgz", + "integrity": "sha512-lrpDziQipxCEeK5kWxvljWYhUvOiB2A9izZd9B2AFarYAkqZshb4lPbRs7zKEic6eGtH8V/2qJW+dPp9OtF6bw==", + "license": "MIT" + }, "node_modules/@types/webidl-conversions": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", @@ -1570,13 +4581,6 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/@ungap/structured-clone": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", - "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", - "dev": true, - "license": "ISC" - }, "node_modules/@unrs/resolver-binding-android-arm-eabi": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz", @@ -1870,9 +4874,9 @@ } }, "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", "dev": true, "license": "MIT", "dependencies": { @@ -1886,11 +4890,58 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-formats/node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -1900,7 +4951,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -1919,6 +4969,18 @@ "dev": true, "license": "Python-2.0" }, + "node_modules/aria-hidden": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", + "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/aria-query": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", @@ -2089,6 +5151,32 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/asn1js": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.1.0.tgz", + "integrity": "sha512-HWYi1THkOqguCiO+vwj09ZkQFD2kKEsejYHAYCtX0D80WVdoDnvRc9IukXX/jeb0T9NRWJnRtqwdbHltnFjwXA==", + "license": "BSD-3-Clause", + "dependencies": { + "pvtsutils": "^1.3.6", + "pvutils": "^1.2.0", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/ast-types": { + "version": "0.16.3", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.16.3.tgz", + "integrity": "sha512-FvWoWYfSCM6kRxCSH+MGLHIKKGRL6A6AW7Zek2O32REPQRdg131428uRTKMBYAeRd3XXAaHDS60Wpri7CdKDrA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/ast-types-flow": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", @@ -2106,6 +5194,15 @@ "node": ">= 0.4" } }, + "node_modules/atomically": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/atomically/-/atomically-1.7.0.tgz", + "integrity": "sha512-Xcz9l0z7y9yQ9rdDaxlmaI4uJHf/T8g9hOEzJcsEqX2SjCj4J20uK7+ldkDHMbpJDK76wF7xEIgxc/vSlsfw5w==", + "license": "MIT", + "engines": { + "node": ">=10.12.0" + } + }, "node_modules/autoprefixer": { "version": "10.4.21", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz", @@ -2186,6 +5283,18 @@ "dev": true, "license": "MIT" }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.35", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.35.tgz", + "integrity": "sha512-GNk8KX52CHqeNKGk8+7tZnQab/2k1t/BgrUHPKNdijzpEhF1saPQZHVKb6m5loSAxB39gyrTYw34Qf9CQ3u1qQ==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/bcryptjs": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.2.tgz", @@ -2210,7 +5319,6 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, "license": "MIT", "dependencies": { "fill-range": "^7.1.1" @@ -2220,9 +5328,9 @@ } }, "node_modules/browserslist": { - "version": "4.25.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.1.tgz", - "integrity": "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==", + "version": "4.29.5", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.29.5.tgz", + "integrity": "sha512-khTmKu5NGYTotqcYo8Bew2k4RAE9PuXRSFRXG2lQBrCEQHyeQ/vX6gKMxfokwywBsLiXZMnHsmWMG1pnO8J7uQ==", "funding": [ { "type": "opencollective", @@ -2239,10 +5347,11 @@ ], "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001726", - "electron-to-chromium": "^1.5.173", - "node-releases": "^2.0.19", - "update-browserslist-db": "^1.1.3" + "baseline-browser-mapping": "^2.11.33", + "caniuse-lite": "^1.0.30001819", + "electron-to-chromium": "^1.5.464", + "node-releases": "^2.0.60", + "update-browserslist-db": "^1.3.4" }, "bin": { "browserslist": "cli.js" @@ -2260,6 +5369,21 @@ "node": ">=16.20.1" } }, + "node_modules/bundle-name": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.1.tgz", + "integrity": "sha512-DdH81/zPLVS11EUgWq3tEu/xn+EzljlMYooDNdzWEnFha3R3NBMpMV1UqYIpjYHV/SgpFKMIX1Oh7o06SjM/oA==", + "license": "MIT", + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/call-bind": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", @@ -2320,10 +5444,19 @@ "node": ">=6" } }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/caniuse-lite": { - "version": "1.0.30001731", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001731.tgz", - "integrity": "sha512-lDdp2/wrOmTRWuoB5DpfNkC0rJDU8DqRa6nYL6HK6sytw70QMopt/NIc/9SM7ylItlBWfACXk0tEn37UWM/+mg==", + "version": "1.0.30001823", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001823.tgz", + "integrity": "sha512-TQ5s/djDjNk1axreUW+4rULH+BmdCMLiJkhaXnO5wqivG7tITkSUj4ItIdzqdZHKaXcHAV8dhH3JxOM7/7bwqw==", "funding": [ { "type": "opencollective", @@ -2367,12 +5500,62 @@ "node": ">=18" } }, + "node_modules/class-variance-authority": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", + "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", + "license": "Apache-2.0", + "dependencies": { + "clsx": "^2.1.1" + }, + "funding": { + "url": "https://polar.sh/cva" + } + }, + "node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/client-only": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", "license": "MIT" }, + "node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, "node_modules/clsx": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", @@ -2382,25 +5565,44 @@ "node": ">=6" } }, - "node_modules/color": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", - "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "node_modules/cmdk": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cmdk/-/cmdk-1.1.1.tgz", + "integrity": "sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg==", "license": "MIT", - "optional": true, "dependencies": { - "color-convert": "^2.0.1", - "color-string": "^1.9.0" + "@radix-ui/react-compose-refs": "^1.1.1", + "@radix-ui/react-dialog": "^1.1.6", + "@radix-ui/react-id": "^1.1.0", + "@radix-ui/react-primitive": "^2.0.2" + }, + "peerDependencies": { + "react": "^18 || ^19 || ^19.0.0-rc", + "react-dom": "^18 || ^19 || ^19.0.0-rc" + } + }, + "node_modules/cn": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/cn/-/cn-0.5.2.tgz", + "integrity": "sha512-wm10E6iuJ37rALOw3D+3cfaXXZSLNMh2uxpQn3dNICqfJM59yXqgbA9/9Iiz155j5kYNLdK6WreNdW3NQtx3OQ==", + "license": "MIT", + "bin": { + "cn": "bin/cn.mjs" }, "engines": { - "node": ">=12.5.0" + "node": ">=22" } }, + "node_modules/code-block-writer": { + "version": "13.0.3", + "resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-13.0.3.tgz", + "integrity": "sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg==", + "license": "MIT" + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "devOptional": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -2413,18 +5615,15 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "devOptional": true, "license": "MIT" }, - "node_modules/color-string": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", - "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "node_modules/commander": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", + "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", "license": "MIT", - "optional": true, - "dependencies": { - "color-name": "^1.0.0", - "simple-swizzle": "^0.2.2" + "engines": { + "node": ">=20" } }, "node_modules/concat-map": { @@ -2434,6 +5633,58 @@ "dev": true, "license": "MIT" }, + "node_modules/conf": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/conf/-/conf-10.2.0.tgz", + "integrity": "sha512-8fLl9F04EJqjSqH+QjITQfJF8BrOVaYr1jewVgSRAEWePfxT0sku4w2hrGQ60BC/TNLGQ2pgxNlTbWQmMPFvXg==", + "license": "MIT", + "dependencies": { + "ajv": "^8.6.3", + "ajv-formats": "^2.1.1", + "atomically": "^1.7.0", + "debounce-fn": "^4.0.0", + "dot-prop": "^6.0.1", + "env-paths": "^2.2.1", + "json-schema-typed": "^7.0.3", + "onetime": "^5.1.2", + "pkg-up": "^3.1.0", + "semver": "^7.3.5" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/conf/node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/conf/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "license": "MIT" + }, "node_modules/cookie": { "version": "0.7.2", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", @@ -2447,7 +5698,6 @@ "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -2464,11 +5714,23 @@ "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==", "license": "MIT" }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/custom-uuid": { @@ -2541,6 +5803,31 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/date-fns": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.4.0.tgz", + "integrity": "sha512-+1UMbeh68lH1SegH83CGWwpb6OHHbpSgr3+s5Eww5M4CAgswBpoWS0AjTOfEJ33HiYKz1hdj/KTFprzXHmq/6w==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, + "node_modules/debounce-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/debounce-fn/-/debounce-fn-4.0.0.tgz", + "integrity": "sha512-8pYCQiL9Xdcg0UPSD3d+0KMlOjp+KGU5EPwYddgzQ7DATsg4fuUDjQtsYLmWjnk2obnNHgV3vE2Y4jejSOJVBQ==", + "license": "MIT", + "dependencies": { + "mimic-fn": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/debug": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", @@ -2558,6 +5845,29 @@ } } }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/dedent": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.2.tgz", + "integrity": "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==", + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -2565,6 +5875,43 @@ "dev": true, "license": "MIT" }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/default-browser": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.1.tgz", + "integrity": "sha512-m1pAzaJgZ/gssEqlOhJkPJp8Xly7QyW6xcrkUa2KKcDeDSEMP7X8xipU3snUcfisTQx0w1AGae+9UtJSfVnXGw==", + "license": "MIT", + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz", + "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/define-data-property": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", @@ -2583,6 +5930,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/define-properties": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", @@ -2601,26 +5960,70 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/detect-libc": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", - "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", "license": "Apache-2.0", "engines": { "node": ">=8" } }, - "node_modules/doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "dev": true, - "license": "Apache-2.0", + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", + "license": "MIT" + }, + "node_modules/diff": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.4.tgz", + "integrity": "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dijkstrajs": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", + "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==", + "license": "MIT" + }, + "node_modules/dot-prop": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-6.0.1.tgz", + "integrity": "sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA==", + "license": "MIT", "dependencies": { - "esutils": "^2.0.2" + "is-obj": "^2.0.0" }, "engines": { - "node": ">=6.0.0" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/dotenv": { + "version": "17.4.2", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", + "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" } }, "node_modules/dunder-proto": { @@ -2639,9 +6042,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.192", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.192.tgz", - "integrity": "sha512-rP8Ez0w7UNw/9j5eSXCe10o1g/8B1P5SM90PCCMVkIRQn2R0LEHWz4Eh9RnxkniuDe1W0cTSOB3MLlkTGDcuCg==", + "version": "1.5.478", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.478.tgz", + "integrity": "sha512-pftG+ma5Lm8O6/p45bhYT6StBN5ziQymaRzmCZwFl8DwWHKPPD93FdOmP1aqftcepolQYvrcyTLa17kE8Yf+jg==", "license": "ISC" }, "node_modules/emoji-regex": { @@ -2665,6 +6068,28 @@ "node": ">=10.13.0" } }, + "node_modules/enquirer": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz", + "integrity": "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==", + "license": "MIT", + "dependencies": { + "ansi-colors": "^4.1.1", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/es-abstract": { "version": "1.24.0", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", @@ -2865,70 +6290,74 @@ } }, "node_modules/eslint": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", - "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "version": "9.39.5", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.5.tgz", + "integrity": "sha512-DgZS62aPLXKlnxILS/AYCoRvHaZeXceIzlXPkkGGzJWSow1aEk0lbTlxUSlyjC8jcaKxAdOnTDz+o1JFSBsyjw==", "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.57.1", - "@humanwhocodes/config-array": "^0.13.0", + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.6", + "@eslint/js": "9.39.5", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "@ungap/structured-clone": "^1.2.0", - "ajv": "^6.12.4", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", + "cross-spawn": "^7.0.6", "debug": "^4.3.2", - "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.2", - "eslint-visitor-keys": "^3.4.3", - "espree": "^9.6.1", - "esquery": "^1.4.2", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", + "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "graphemer": "^1.4.0", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", - "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", + "minimatch": "^3.1.5", "natural-compare": "^1.4.0", - "optionator": "^0.9.3", - "strip-ansi": "^6.0.1", - "text-table": "^0.2.0" + "optionator": "^0.9.3" }, "bin": { "eslint": "bin/eslint.js" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://opencollective.com/eslint" + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } } }, "node_modules/eslint-config-next": { - "version": "15.3.2", - "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-15.3.2.tgz", - "integrity": "sha512-FerU4DYccO4FgeYFFglz0SnaKRe1ejXQrDb8kWUkTAg036YWi+jUsgg4sIGNCDhAsDITsZaL4MzBWKB6f4G1Dg==", + "version": "15.5.29", + "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-15.5.29.tgz", + "integrity": "sha512-44lyVuWyyuYgLeuJvuHSMcvHsfC/JLae6C2xCdi6neWAxk94vKm5qGBFmTrGQavQ2JIL2Jrll16incpNlKOJbQ==", "dev": true, "license": "MIT", "dependencies": { - "@next/eslint-plugin-next": "15.3.2", + "@next/eslint-plugin-next": "15.5.29", "@rushstack/eslint-patch": "^1.10.3", "@typescript-eslint/eslint-plugin": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", "@typescript-eslint/parser": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", @@ -3219,9 +6648,9 @@ } }, "node_modules/eslint-scope": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", - "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -3229,7 +6658,7 @@ "estraverse": "^5.2.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" @@ -3248,64 +6677,19 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint/node_modules/@eslint/eslintrc": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", - "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.6.0", - "globals": "^13.19.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint/node_modules/espree": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", - "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.9.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" - }, + "license": "Apache-2.0", "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint/node_modules/globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "type-fest": "^0.20.2" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/espree": { "version": "10.4.0", "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", @@ -3337,6 +6721,19 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/esquery": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", @@ -3383,11 +6780,36 @@ "node": ">=0.10.0" } }, + "node_modules/execa": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-9.6.1.tgz", + "integrity": "sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==", + "license": "MIT", + "dependencies": { + "@sindresorhus/merge-streams": "^4.0.0", + "cross-spawn": "^7.0.6", + "figures": "^6.1.0", + "get-stream": "^9.0.0", + "human-signals": "^8.0.1", + "is-plain-obj": "^4.1.0", + "is-stream": "^4.0.1", + "npm-run-path": "^6.0.0", + "pretty-ms": "^9.2.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^4.0.0", + "yoctocolors": "^2.1.1" + }, + "engines": { + "node": "^18.19.0 || >=20.5.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, "license": "MIT" }, "node_modules/fast-glob": { @@ -3434,34 +6856,63 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-uri": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.9.tgz", + "integrity": "sha512-hS/0Wt3Ya043oKXs0sIxrMpKQITXhMLIrVAKKXi4gozGC9v7YRWVnXIZzjNPTO02M9BdxLCWqK1yRXgNziVyJQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/fastq": { "version": "1.19.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", - "dev": true, "license": "ISC", "dependencies": { "reusify": "^1.0.4" } }, + "node_modules/figures": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz", + "integrity": "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==", + "license": "MIT", + "dependencies": { + "is-unicode-supported": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/file-entry-cache": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", - "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", "dev": true, "license": "MIT", "dependencies": { - "flat-cache": "^3.0.4" + "flat-cache": "^4.0.0" }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": ">=16.0.0" } }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" @@ -3488,24 +6939,23 @@ } }, "node_modules/flat-cache": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", - "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", "dev": true, "license": "MIT", "dependencies": { "flatted": "^3.2.9", - "keyv": "^4.5.3", - "rimraf": "^3.0.2" + "keyv": "^4.5.4" }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": ">=16" } }, "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", + "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", "dev": true, "license": "ISC" }, @@ -3538,12 +6988,19 @@ "url": "https://github.com/sponsors/rawify" } }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true, - "license": "ISC" + "node_modules/fs-extra": { + "version": "11.4.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.4.1.tgz", + "integrity": "sha512-KYAb4c9BJQI6QqGKthV68OHe0badztdXJWKo0WtBA9IuCFPTKvE5ZdUBglP833aMjhaSPNO4A5j/EkzZtGlKjA==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } }, "node_modules/function-bind": { "version": "1.1.2", @@ -3586,6 +7043,51 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/fuzzysort": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fuzzysort/-/fuzzysort-3.1.0.tgz", + "integrity": "sha512-sR9BNCjBg6LNgwvxlBd0sBABvQitkLzoVY9MYYROQVX/FvfJ4Mai9LsGhDgd8qYdds0bY77VzYd5iuB+v5rwQQ==", + "license": "MIT" + }, + "node_modules/geist": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/geist/-/geist-1.9.2.tgz", + "integrity": "sha512-UU1x2q5Khob0+i4csFvXk1Ki4DePdu7xISuXyEcj1tzQMg64oN7Kbuy4e5z5QhcUWbbvOMJNs3ZxYQqpsEMV3w==", + "license": "SIL OPEN FONT LICENSE", + "peerDependencies": { + "next": ">=13.2.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.7.0.tgz", + "integrity": "sha512-XjH1AECxf0giL2V1aU8vKyRR2ppRUb5c0EvT7zuJTokQ74bNo52zOtghqdWIqrhUD79fo3x0WfKZdOqxF6LG1Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -3611,6 +7113,27 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/get-own-enumerable-keys": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/get-own-enumerable-keys/-/get-own-enumerable-keys-1.0.0.tgz", + "integrity": "sha512-PKsK2FSrQCyxcGHsGrLDcK0lx+0Ke+6e8KFFozA9/fIQLhQzPaRvJFdcz7+Axg3jUH/Mq+NI4xa5u/UT2tQskA==", + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", @@ -3625,6 +7148,22 @@ "node": ">= 0.4" } }, + "node_modules/get-stream": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz", + "integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==", + "license": "MIT", + "dependencies": { + "@sec-ant/readable-stream": "^0.4.1", + "is-stream": "^4.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-symbol-description": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", @@ -3656,28 +7195,6 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, - "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -3738,7 +7255,6 @@ "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, "license": "ISC" }, "node_modules/graphemer": { @@ -3842,11 +7358,19 @@ "node": ">= 0.4" } }, + "node_modules/human-signals": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-8.0.1.tgz", + "integrity": "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 4" @@ -3879,25 +7403,16 @@ "node": ">=0.8.19" } }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "dev": true, - "license": "ISC", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" + "node_modules/input-otp": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/input-otp/-/input-otp-1.5.0.tgz", + "integrity": "sha512-3AcfdW1sNG0FmSA5hHMBXG7jNW5CdcdKs8ln8JOmn6S2SRkoXoJx+2UwC+ZvjflfnOdHJx0SfQ9/YjlxVYLtGQ==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc" } }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true, - "license": "ISC" - }, "node_modules/internal-slot": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", @@ -3913,6 +7428,15 @@ "node": ">= 0.4" } }, + "node_modules/ip-address": { + "version": "10.7.6", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.6.tgz", + "integrity": "sha512-00LYkM6byILa9eQIx6qoaXCM40xTLd56AZ4vyaPW3YTyb49VZYPo5xsiAq4tuQ54GH4R7gVdYScJ1B7B77PuTg==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/is-array-buffer": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", @@ -3931,13 +7455,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-arrayish": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", - "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", - "license": "MIT", - "optional": true - }, "node_modules/is-async-function": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", @@ -4065,11 +7582,25 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -4091,6 +7622,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-generator-function": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", @@ -4114,7 +7654,6 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" @@ -4123,6 +7662,48 @@ "node": ">=0.10.0" } }, + "node_modules/is-in-ssh": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-in-ssh/-/is-in-ssh-1.0.0.tgz", + "integrity": "sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-interactive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", + "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-map": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", @@ -4153,7 +7734,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.12.0" @@ -4176,16 +7756,27 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-path-inside": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", - "dev": true, + "node_modules/is-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", + "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", "license": "MIT", "engines": { "node": ">=8" } }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-regex": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", @@ -4205,6 +7796,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-regexp": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-3.1.0.tgz", + "integrity": "sha512-rbku49cWloU5bSMI+zaRaXdQHXnthP6DZ/vLnfdSKyL4zUzuWnomtOEiZZOd+ioQ+avFo/qau3KPTc7Fjy1uPA==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-set": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", @@ -4234,6 +7837,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-stream": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz", + "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-string": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", @@ -4285,6 +7900,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-weakmap": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", @@ -4331,6 +7958,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-wsl": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", + "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/isarray": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", @@ -4342,7 +7984,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, "license": "ISC" }, "node_modules/iterator.prototype": { @@ -4389,10 +8030,20 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", + "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -4401,6 +8052,18 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -4415,6 +8078,12 @@ "dev": true, "license": "MIT" }, + "node_modules/json-schema-typed": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-7.0.3.tgz", + "integrity": "sha512-7DE8mpG+/fVw+dTpjbxnx47TaMnDfOI1jwft9g1VybltZCduyRQPJPvc+zzKY9WPHxhPWczyFuYa6I8Mw4iU5A==", + "license": "BSD-2-Clause" + }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", @@ -4435,6 +8104,18 @@ "json5": "lib/cli.js" } }, + "node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, "node_modules/jsx-ast-utils": { "version": "3.3.5", "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", @@ -4470,6 +8151,15 @@ "json-buffer": "3.0.1" } }, + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/language-subtag-registry": { "version": "0.3.23", "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", @@ -4755,6 +8445,46 @@ "dev": true, "license": "MIT" }, + "node_modules/log-symbols": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz", + "integrity": "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==", + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "is-unicode-supported": "^1.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-symbols/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/log-symbols/node_modules/is-unicode-supported": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", + "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -4814,11 +8544,16 @@ "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==", "license": "MIT" }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "license": "MIT" + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 8" @@ -4828,20 +8563,40 @@ "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mimic-fn": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-3.1.0.tgz", + "integrity": "sha512-Ysbi9uYW9hFyfrThdDEQuykN4Ey6BuwPD2kpI5ES/nFTDn/98yxYNLZJcgUAKPT/mcrLLKaGzJR9YVxJrIdASQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "license": "MIT", "engines": { - "node": ">=8.6" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -4855,7 +8610,6 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -4901,14 +8655,14 @@ } }, "node_modules/mongodb": { - "version": "6.17.0", - "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.17.0.tgz", - "integrity": "sha512-neerUzg/8U26cgruLysKEjJvoNSXhyID3RvzvdcpsIi2COYM3FS3o9nlH7fxFtefTb942dX3W9i37oPfCVj4wA==", + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.20.0.tgz", + "integrity": "sha512-Tl6MEIU3K4Rq3TSHd+sZQqRBoGlFsOgNrH5ltAcFBV62Re3Fd+FcaVf8uSEQFOJ51SDowDVttBTONMfoYWrWlQ==", "license": "Apache-2.0", "dependencies": { - "@mongodb-js/saslprep": "^1.1.9", + "@mongodb-js/saslprep": "^1.3.0", "bson": "^6.10.4", - "mongodb-connection-string-url": "^3.0.0" + "mongodb-connection-string-url": "^3.0.2" }, "engines": { "node": ">=16.20.1" @@ -4919,7 +8673,7 @@ "gcp-metadata": "^5.2.0", "kerberos": "^2.0.1", "mongodb-client-encryption": ">=6.0.0 <7", - "snappy": "^7.2.2", + "snappy": "^7.3.2", "socks": "^2.7.1" }, "peerDependenciesMeta": { @@ -4957,16 +8711,16 @@ } }, "node_modules/mongoose": { - "version": "8.16.5", - "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.16.5.tgz", - "integrity": "sha512-Ey84Jf1c426FEMj16+j/VqFKupFN2Ey5uAy6TTAN9HlFP4OcunL7j9O7vTuwAipzlZdjctxP0OK9MRJ4Aa/jNg==", + "version": "8.24.8", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.24.8.tgz", + "integrity": "sha512-p6g/mvvW2PJlC3lgzQJ7PECArfaOTKWJVG/KHf67WYU3MvJ72ZWdv5u7syFRe881IjALMZpKwffNE97G10HTzg==", "license": "MIT", "dependencies": { "bson": "^6.10.4", "kareem": "2.6.3", - "mongodb": "~6.17.0", + "mongodb": "~6.20.0", "mpath": "0.9.0", - "mquery": "5.0.0", + "mquery": "5.0.1", "ms": "2.1.3", "sift": "17.1.3" }, @@ -4988,9 +8742,9 @@ } }, "node_modules/mquery": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/mquery/-/mquery-5.0.0.tgz", - "integrity": "sha512-iQMncpmEK8R8ncT8HJGsGc9Dsp8xcgYMVSbs5jgnm1lFHTZqMJTUWTDx1LBO8+mK3tPNZWFLBghQEIOULSTHZg==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mquery/-/mquery-5.0.1.tgz", + "integrity": "sha512-C99lWV0TFI34pS008jBsi3JrE2xZPnTgUw5yApQR71nRP0hxLy8D5m1Lbbk83c8AQWJwdJ0329HzFtZ1MRTUIw==", "license": "MIT", "dependencies": { "debug": "4.x" @@ -5006,9 +8760,9 @@ "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "version": "3.3.20", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.20.tgz", + "integrity": "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==", "funding": [ { "type": "github", @@ -5047,12 +8801,12 @@ "license": "MIT" }, "node_modules/next": { - "version": "15.4.4", - "resolved": "https://registry.npmjs.org/next/-/next-15.4.4.tgz", - "integrity": "sha512-kNcubvJjOL9yUOfwtZF3HfDhuhp+kVD+FM2A6Tyua1eI/xfmY4r/8ZS913MMz+oWKDlbps/dQOWdDricuIkXLw==", + "version": "15.5.29", + "resolved": "https://registry.npmjs.org/next/-/next-15.5.29.tgz", + "integrity": "sha512-93AMszTCdi9Az7fD5VzcWy46SHCdQF45+i53K0LpDc4dDXCwB9P1SxP1wBYZKW6dF3HJnz41jyEni1vmgCluiw==", "license": "MIT", "dependencies": { - "@next/env": "15.4.4", + "@next/env": "15.5.29", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", @@ -5065,15 +8819,15 @@ "node": "^18.18.0 || ^19.8.0 || >= 20.0.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "15.4.4", - "@next/swc-darwin-x64": "15.4.4", - "@next/swc-linux-arm64-gnu": "15.4.4", - "@next/swc-linux-arm64-musl": "15.4.4", - "@next/swc-linux-x64-gnu": "15.4.4", - "@next/swc-linux-x64-musl": "15.4.4", - "@next/swc-win32-arm64-msvc": "15.4.4", - "@next/swc-win32-x64-msvc": "15.4.4", - "sharp": "^0.34.3" + "@next/swc-darwin-arm64": "15.5.29", + "@next/swc-darwin-x64": "15.5.29", + "@next/swc-linux-arm64-gnu": "15.5.29", + "@next/swc-linux-arm64-musl": "15.5.29", + "@next/swc-linux-x64-gnu": "15.5.29", + "@next/swc-linux-x64-musl": "15.5.29", + "@next/swc-win32-arm64-msvc": "15.5.29", + "@next/swc-win32-x64-msvc": "15.5.29", + "sharp": "^0.34.3 || ^0.35.5" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", @@ -5099,9 +8853,9 @@ } }, "node_modules/next-auth": { - "version": "4.24.11", - "resolved": "https://registry.npmjs.org/next-auth/-/next-auth-4.24.11.tgz", - "integrity": "sha512-pCFXzIDQX7xmHFs4KVH4luCjaCbuPRtZ9oBUjUhOk84mZ9WVPf94n87TxYI4rSRf9HmfHEF8Yep3JrYDVOo3Cw==", + "version": "4.24.15", + "resolved": "https://registry.npmjs.org/next-auth/-/next-auth-4.24.15.tgz", + "integrity": "sha512-NnjYtjrSOAx/TIVFGTX4IfI/9yHnNpi4B7FuLUwuV20v2Zxgr2OGP/YN0ynJuI7y8QOnTBPitfOdEXZrVvhIuA==", "license": "ISC", "dependencies": { "@babel/runtime": "^7.20.13", @@ -5112,12 +8866,12 @@ "openid-client": "^5.4.0", "preact": "^10.6.3", "preact-render-to-string": "^5.1.19", - "uuid": "^8.3.2" + "uuid": "^11.1.1" }, "peerDependencies": { - "@auth/core": "0.34.2", - "next": "^12.2.5 || ^13 || ^14 || ^15", - "nodemailer": "^6.6.5", + "@auth/core": "0.34.3", + "next": "^12.2.5 || ^13 || ^14 || ^15 || ^16", + "nodemailer": "^7.0.7", "react": "^17.0.2 || ^18 || ^19", "react-dom": "^17.0.2 || ^18 || ^19" }, @@ -5169,10 +8923,13 @@ } }, "node_modules/node-releases": { - "version": "2.0.19", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", - "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", - "license": "MIT" + "version": "2.0.63", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.63.tgz", + "integrity": "sha512-6li2QanLsAXpIH2R6j77MzRyvOyrvdTqlKtO4CgBdHGNvN99ZZfyxszHc5o5+IZBWD5u6Hzwg83JoGXmqZNG9Q==", + "license": "MIT", + "engines": { + "node": ">=18" + } }, "node_modules/normalize-range": { "version": "0.1.2", @@ -5183,6 +8940,34 @@ "node": ">=0.10.0" } }, + "node_modules/npm-run-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-6.0.0.tgz", + "integrity": "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==", + "license": "MIT", + "dependencies": { + "path-key": "^4.0.0", + "unicorn-magic": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/oauth": { "version": "0.9.15", "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.9.15.tgz", @@ -5231,6 +9016,15 @@ "node": ">= 0.4" } }, + "node_modules/object-treeify": { + "version": "1.1.33", + "resolved": "https://registry.npmjs.org/object-treeify/-/object-treeify-1.1.33.tgz", + "integrity": "sha512-EFVjAYfzWqWsBMRHPMAXLCDIJnpMhdWAqR7xG6M6a2cs6PMFpl/+Z20w9zDW4vkxOFfddegBKq9Rehd0bxWE7A==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, "node_modules/object.assign": { "version": "4.1.7", "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", @@ -5330,14 +9124,48 @@ "node": "^10.13.0 || >=12.0.0" } }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, - "license": "ISC", + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/onetime/node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/open": { + "version": "11.0.4", + "resolved": "https://registry.npmjs.org/open/-/open-11.0.4.tgz", + "integrity": "sha512-++Zlftm0kVLPmzC06t6epuWmcRMDbI4z5P3NNX979WA/k23+NtSOynEGzsVfZwguKw2mi5umVgnBlJQMwRz4Pg==", + "license": "MIT", "dependencies": { - "wrappy": "1" + "default-browser": "^5.5.1", + "define-lazy-prop": "^3.0.0", + "is-in-ssh": "^1.0.0", + "is-inside-container": "^1.0.0", + "powershell-utils": "^0.2.1", + "wsl-utils": "^1.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/openid-client": { @@ -5373,6 +9201,103 @@ "node": ">= 0.8.0" } }, + "node_modules/ora": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/ora/-/ora-8.2.0.tgz", + "integrity": "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==", + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "cli-cursor": "^5.0.0", + "cli-spinners": "^2.9.2", + "is-interactive": "^2.0.0", + "is-unicode-supported": "^2.0.0", + "log-symbols": "^6.0.0", + "stdin-discarder": "^0.2.2", + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/ansi-regex": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.5.0.tgz", + "integrity": "sha512-zhvbbbdt1kbw7EJ9OQoxmQcuaNR9P5kTj7e/YdmOSU10aHWbycofJxA6jKfoEy3vlLdctOF9ZHaxLtmSm998wQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ora/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/ora/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "license": "MIT" + }, + "node_modules/ora/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/otpauth": { + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/otpauth/-/otpauth-9.6.0.tgz", + "integrity": "sha512-qQXHTxcXohDhQjF1ZEbUVEpTIssFL2rd+2kHfw/McIVCbTLD6qAsbTCQxpXRLyeyUw6Ipjpykw4dWZL/zfIVqA==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "2.4.0" + }, + "funding": { + "url": "https://github.com/hectorm/otpauth?sponsor=1" + } + }, "node_modules/own-keys": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", @@ -5423,6 +9348,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -5436,31 +9370,37 @@ "node": ">=6" } }, - "node_modules/path-exists": { + "node_modules/parse-ms": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, + "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz", + "integrity": "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==", "license": "MIT", "engines": { - "node": ">=8" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/path-is-absolute": { + "node_modules/path-browserify": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "license": "MIT" + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -5480,10 +9420,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "license": "MIT", "engines": { "node": ">=8.6" @@ -5492,6 +9431,88 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pkg-up": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/pkg-up/-/pkg-up-3.1.0.tgz", + "integrity": "sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==", + "license": "MIT", + "dependencies": { + "find-up": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-up/node_modules/find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "license": "MIT", + "dependencies": { + "locate-path": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/pkg-up/node_modules/locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "license": "MIT", + "dependencies": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/pkg-up/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-up/node_modules/p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "license": "MIT", + "dependencies": { + "p-limit": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/pkg-up/node_modules/path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/pngjs": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", + "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==", + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -5503,9 +9524,9 @@ } }, "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "version": "8.5.32", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.32.tgz", + "integrity": "sha512-LJlKCCAUFKOuZxipUdYw1NzIITEsPcgyOG9LkvRJvuKj/bRzbuRY0ARXvL0pWGpsZvn8jku+LMep2eU0seHp8g==", "funding": [ { "type": "opencollective", @@ -5522,28 +9543,61 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.11", + "nanoid": "^3.3.20", "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" + "source-map-js": "^1.2.2" }, "engines": { "node": "^10 || ^12 || >=14" } }, + "node_modules/postcss-selector-parser": { + "version": "7.1.7", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.7.tgz", + "integrity": "sha512-wWBro/7hJ40ye6TPiXkUQoXKCw23xSsjriUvqd7Uls9Sso3oLiON1flc18cCdnjgCRXw4fBVqUJJeOdDTh3P3g==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/postcss-value-parser": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "license": "MIT" }, + "node_modules/powershell-utils": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/powershell-utils/-/powershell-utils-0.2.1.tgz", + "integrity": "sha512-C+y9x90UElAddDZmV4qOx9W53B61PO7cIqWz2dQsWlwswuq4mr8NEwytdGKboYbQlGZ3awrkTeNvcZiZNHnQ8A==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/preact": { - "version": "10.27.0", - "resolved": "https://registry.npmjs.org/preact/-/preact-10.27.0.tgz", - "integrity": "sha512-/DTYoB6mwwgPytiqQTh/7SFRL98ZdiD8Sk8zIUVOxtwq4oWcwrcd1uno9fE/zZmUaUrFNYzbH14CPebOz9tZQw==", + "version": "10.29.8", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.29.8.tgz", + "integrity": "sha512-ej2aVZ+vZ8WO7tvlQWRM9N63A0KzF9q4mWJfDUHgYaIofWY9hu74QdnQrjoPMmZi2/nZ5gN0bJCQF49xQqx09Q==", "license": "MIT", "funding": { "type": "opencollective", "url": "https://opencollective.com/preact" + }, + "peerDependencies": { + "preact-render-to-string": ">=5" + }, + "peerDependenciesMeta": { + "preact-render-to-string": { + "optional": true + } } }, "node_modules/preact-render-to-string": { @@ -5565,15 +9619,52 @@ "dev": true, "license": "MIT", "engines": { - "node": ">= 0.8.0" + "node": ">= 0.8.0" + } + }, + "node_modules/pretty-format": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-3.8.0.tgz", + "integrity": "sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==", + "license": "MIT" + }, + "node_modules/pretty-ms": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.3.1.tgz", + "integrity": "sha512-HzMy3Geq23nVALD/M2LliU+F+M+gVNsvkQWWqeBZ8HDiCgzo6YPJ/Omrmtq24EFrIsk0a3EkQGEd7bDOo+IhGA==", + "license": "MIT", + "dependencies": { + "parse-ms": "^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/prompts/node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "license": "MIT", + "engines": { + "node": ">=6" } }, - "node_modules/pretty-format": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-3.8.0.tgz", - "integrity": "sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==", - "license": "MIT" - }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -5595,11 +9686,45 @@ "node": ">=6" } }, + "node_modules/pvtsutils": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.3.6.tgz", + "integrity": "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.8.1" + } + }, + "node_modules/pvutils": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pvutils/-/pvutils-1.2.0.tgz", + "integrity": "sha512-BbubeCEyTuQjVMakvJQ/Sxbc93F2pwmbsxONT/ZRrwU7Ua38d8unYTwXpTVLAKJ4BDuH9IGztCjQcd/N/39Dvg==", + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/qrcode": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz", + "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==", + "license": "MIT", + "dependencies": { + "dijkstrajs": "^1.0.1", + "pngjs": "^5.0.0", + "yargs": "^15.3.1" + }, + "bin": { + "qrcode": "bin/qrcode" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, "funding": [ { "type": "github", @@ -5616,6 +9741,83 @@ ], "license": "MIT" }, + "node_modules/radix-ui": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/radix-ui/-/radix-ui-1.8.1.tgz", + "integrity": "sha512-HPfpVGizO9xANlsFSauruLhRe/3+iOt3vPX0MXYle/UQi3gorQrSYtBcdiv0BZYewcl2rvL59qrIuLFMGAmUrw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.2.0", + "@radix-ui/react-accessible-icon": "1.2.0", + "@radix-ui/react-accordion": "1.3.1", + "@radix-ui/react-alert-dialog": "1.2.1", + "@radix-ui/react-arrow": "1.1.17", + "@radix-ui/react-aspect-ratio": "1.2.0", + "@radix-ui/react-avatar": "1.3.1", + "@radix-ui/react-checkbox": "1.4.1", + "@radix-ui/react-collapsible": "1.2.1", + "@radix-ui/react-collection": "1.2.0", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.3.0", + "@radix-ui/react-context-menu": "2.4.1", + "@radix-ui/react-dialog": "1.3.1", + "@radix-ui/react-direction": "1.2.0", + "@radix-ui/react-dismissable-layer": "1.2.1", + "@radix-ui/react-dropdown-menu": "2.2.1", + "@radix-ui/react-focus-guards": "1.2.0", + "@radix-ui/react-focus-scope": "1.3.1", + "@radix-ui/react-form": "0.3.0", + "@radix-ui/react-hover-card": "1.2.1", + "@radix-ui/react-label": "2.1.17", + "@radix-ui/react-menu": "2.2.1", + "@radix-ui/react-menubar": "1.2.1", + "@radix-ui/react-navigation-menu": "1.4.1", + "@radix-ui/react-one-time-password-field": "0.2.1", + "@radix-ui/react-password-toggle-field": "0.2.1", + "@radix-ui/react-popover": "1.3.1", + "@radix-ui/react-popper": "1.4.0", + "@radix-ui/react-portal": "1.2.0", + "@radix-ui/react-presence": "1.1.11", + "@radix-ui/react-primitive": "2.2.0", + "@radix-ui/react-progress": "1.2.1", + "@radix-ui/react-radio-group": "1.5.1", + "@radix-ui/react-roving-focus": "1.2.1", + "@radix-ui/react-scroll-area": "1.4.0", + "@radix-ui/react-select": "2.4.1", + "@radix-ui/react-separator": "1.2.0", + "@radix-ui/react-slider": "1.6.1", + "@radix-ui/react-slot": "1.5.0", + "@radix-ui/react-switch": "1.4.1", + "@radix-ui/react-tabs": "1.2.1", + "@radix-ui/react-toast": "1.3.1", + "@radix-ui/react-toggle": "1.2.1", + "@radix-ui/react-toggle-group": "1.2.1", + "@radix-ui/react-toolbar": "1.2.1", + "@radix-ui/react-tooltip": "1.4.1", + "@radix-ui/react-use-callback-ref": "1.1.5", + "@radix-ui/react-use-controllable-state": "1.3.0", + "@radix-ui/react-use-effect-event": "0.0.5", + "@radix-ui/react-use-escape-keydown": "1.1.6", + "@radix-ui/react-use-is-hydrated": "0.1.3", + "@radix-ui/react-use-layout-effect": "1.1.4", + "@radix-ui/react-use-size": "1.1.5", + "@radix-ui/react-visually-hidden": "1.2.13" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/rate-limiter-flexible": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/rate-limiter-flexible/-/rate-limiter-flexible-7.2.0.tgz", @@ -5634,6 +9836,32 @@ "node": ">=0.10.0" } }, + "node_modules/react-day-picker": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-10.0.2.tgz", + "integrity": "sha512-QetiKtZWE9Uaw/pM/SYNv6WPYOTRnSJ2puL0GgwkmUFMIwiY0dtjH1W4B7DOVXVNQM0ZOClGpH/heOfBSzWeiA==", + "license": "MIT", + "dependencies": { + "@date-fns/tz": "^1.4.1", + "date-fns": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/gpbl" + }, + "peerDependencies": { + "@types/react": ">=16.8.0", + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/react-dom": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", @@ -5654,6 +9882,97 @@ "dev": true, "license": "MIT" }, + "node_modules/react-remove-scroll": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz", + "integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==", + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.3", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "license": "MIT", + "dependencies": { + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-style-singleton": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "license": "MIT", + "dependencies": { + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/recast": { + "version": "0.23.21", + "resolved": "https://registry.npmjs.org/recast/-/recast-0.23.21.tgz", + "integrity": "sha512-mFAyJq9vUbSTARLZUvAEf1z3YxlvAwswbmxMx2mPA/MSm4KmpwvwvhsH/NIrZhyOuwD60Lzyw2qh83uCbgTPYw==", + "license": "MIT", + "dependencies": { + "ast-types": "^0.16.1", + "esprima": "~4.0.0", + "source-map": "~0.6.1", + "tiny-invariant": "^1.3.3", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">= 4" + } + }, + "node_modules/reflect-metadata": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", + "license": "Apache-2.0" + }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -5698,6 +10017,30 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "license": "ISC" + }, "node_modules/resolve": { "version": "1.22.10", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", @@ -5739,39 +10082,63 @@ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, + "node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/restore-cursor/node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/reusify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", - "dev": true, "license": "MIT", "engines": { "iojs": ">=1.0.0", "node": ">=0.10.0" } }, - "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", - "dev": true, - "license": "ISC", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" + "node_modules/run-applescript": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", + "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", + "license": "MIT", + "engines": { + "node": ">=18" }, "funding": { - "url": "https://github.com/sponsors/isaacs" + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, "funding": [ { "type": "github", @@ -5856,10 +10223,9 @@ } }, "node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", - "devOptional": true, + "version": "7.8.6", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.6.tgz", + "integrity": "sha512-9lpSSU4Mly3mrtruwLs1eLK6+gzbzqeBFwRNjIjE+9LuDpWdYctrQlQ3ej1Ilgi7gknpsTRnPoLP8DpCHqHIJw==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -5868,6 +10234,18 @@ "node": ">=10" } }, + "node_modules/server-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/server-only/-/server-only-0.0.1.tgz", + "integrity": "sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==", + "license": "MIT" + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "license": "ISC" + }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -5917,54 +10295,172 @@ "node": ">= 0.4" } }, + "node_modules/shadcn": { + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/shadcn/-/shadcn-4.23.0.tgz", + "integrity": "sha512-lKgq7QPFgkV/6EOrMCwvEn7ZAw/YnM/grhxzJmK4cZdw3E4F3Ut2EmWvrn2E+vzJp1y/76mxU185OSkXo9Ff+Q==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/parser": "^7.28.0", + "@babel/plugin-transform-typescript": "^7.28.0", + "@babel/preset-typescript": "^7.27.1", + "@dotenvx/dotenvx": "^1.48.4", + "@modelcontextprotocol/server": "^2.3.1", + "@shadcn/registry": "0.2.1", + "@types/validate-npm-package-name": "^4.0.2", + "browserslist": "^4.29.3", + "cn": "^0.2.4", + "commander": "^14.0.0", + "dedent": "^1.6.0", + "deepmerge": "^4.3.1", + "diff": "^8.0.2", + "execa": "^9.6.0", + "fast-glob": "^3.3.3", + "fs-extra": "^11.3.1", + "fuzzysort": "^3.1.0", + "kleur": "^4.1.5", + "open": "^11.0.4", + "ora": "^8.2.0", + "postcss": "^8.5.23", + "postcss-selector-parser": "^7.1.6", + "prompts": "^2.4.2", + "recast": "^0.23.11", + "socks": "^2.8.8", + "stringify-object": "^5.0.0", + "ts-morph": "^26.0.0", + "tsconfig-paths": "^4.2.0", + "undici": "^7.30.1", + "validate-npm-package-name": "^7.0.1", + "zod": "^4.2.0" + }, + "bin": { + "shadcn": "dist/index.js" + }, + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/shadcn/node_modules/cn": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/cn/-/cn-0.2.6.tgz", + "integrity": "sha512-+i4L0zUGgRcEnhsxueVrP7iBGxBx5iD0WOTYg1MwFEu2ZyCmH5Ov2V1cul2Ht5UchRQQgftCf4be/RxspuW6QQ==", + "license": "MIT", + "bin": { + "cn": "bin/cn.mjs" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/shadcn/node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/shadcn/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/shadcn/node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/shadcn/node_modules/tsconfig-paths": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", + "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", + "license": "MIT", + "dependencies": { + "json5": "^2.2.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/sharp": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.3.tgz", - "integrity": "sha512-eX2IQ6nFohW4DbvHIOLRB3MHFpYqaqvXd3Tp5e/T/dSH83fxaNJQRvDMhASmkNTsNTVF2/OOopzRCt7xokgPfg==", - "hasInstallScript": true, + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.5.tgz", + "integrity": "sha512-Ywn4OnzGukp7CDMrp08RQ50YKmuwG47brZgIVPTvBaaAfQlRlygrRqSrxdCiL9M+LlzLBiJ68IR1QqvzHyjC7g==", "license": "Apache-2.0", "optional": true, "dependencies": { - "color": "^4.2.3", - "detect-libc": "^2.0.4", - "semver": "^7.7.2" + "@img/colour": "^1.1.0", + "detect-libc": "^2.1.2", + "semver": "^7.8.5" }, "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.34.3", - "@img/sharp-darwin-x64": "0.34.3", - "@img/sharp-libvips-darwin-arm64": "1.2.0", - "@img/sharp-libvips-darwin-x64": "1.2.0", - "@img/sharp-libvips-linux-arm": "1.2.0", - "@img/sharp-libvips-linux-arm64": "1.2.0", - "@img/sharp-libvips-linux-ppc64": "1.2.0", - "@img/sharp-libvips-linux-s390x": "1.2.0", - "@img/sharp-libvips-linux-x64": "1.2.0", - "@img/sharp-libvips-linuxmusl-arm64": "1.2.0", - "@img/sharp-libvips-linuxmusl-x64": "1.2.0", - "@img/sharp-linux-arm": "0.34.3", - "@img/sharp-linux-arm64": "0.34.3", - "@img/sharp-linux-ppc64": "0.34.3", - "@img/sharp-linux-s390x": "0.34.3", - "@img/sharp-linux-x64": "0.34.3", - "@img/sharp-linuxmusl-arm64": "0.34.3", - "@img/sharp-linuxmusl-x64": "0.34.3", - "@img/sharp-wasm32": "0.34.3", - "@img/sharp-win32-arm64": "0.34.3", - "@img/sharp-win32-ia32": "0.34.3", - "@img/sharp-win32-x64": "0.34.3" + "@img/sharp-darwin-arm64": "0.35.5", + "@img/sharp-darwin-x64": "0.35.5", + "@img/sharp-freebsd-wasm32": "0.35.5", + "@img/sharp-libvips-darwin-arm64": "1.3.4", + "@img/sharp-libvips-darwin-x64": "1.3.4", + "@img/sharp-libvips-linux-arm": "1.3.4", + "@img/sharp-libvips-linux-arm64": "1.3.4", + "@img/sharp-libvips-linux-ppc64": "1.3.4", + "@img/sharp-libvips-linux-riscv64": "1.3.4", + "@img/sharp-libvips-linux-s390x": "1.3.4", + "@img/sharp-libvips-linux-x64": "1.3.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.3.4", + "@img/sharp-libvips-linuxmusl-x64": "1.3.4", + "@img/sharp-linux-arm": "0.35.5", + "@img/sharp-linux-arm64": "0.35.5", + "@img/sharp-linux-ppc64": "0.35.5", + "@img/sharp-linux-riscv64": "0.35.5", + "@img/sharp-linux-s390x": "0.35.5", + "@img/sharp-linux-x64": "0.35.5", + "@img/sharp-linuxmusl-arm64": "0.35.5", + "@img/sharp-linuxmusl-x64": "0.35.5", + "@img/sharp-webcontainers-wasm32": "0.35.5", + "@img/sharp-win32-arm64": "0.35.5", + "@img/sharp-win32-ia32": "0.35.5", + "@img/sharp-win32-x64": "0.35.5" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" @@ -5977,7 +10473,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -6065,14 +10560,65 @@ "integrity": "sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==", "license": "MIT" }, - "node_modules/simple-swizzle": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", - "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/simple-icons": { + "version": "16.36.0", + "resolved": "https://registry.npmjs.org/simple-icons/-/simple-icons-16.36.0.tgz", + "integrity": "sha512-+VNPPkqSOLidFeSgBKUWX4n50WPpDC09hav+BgS26anIF1oQOlGBb8LvwGR+6fYRjo+B6CiKzNBceMPkSVXANQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/simple-icons" + }, + { + "type": "github", + "url": "https://github.com/sponsors/simple-icons" + } + ], + "license": "CC0-1.0", + "engines": { + "node": ">=0.12.18" + } + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "license": "MIT" + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.10", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.10.tgz", + "integrity": "sha512-e0VyvkVTwVYViNovRkZ9aodhxVlyoMn7eJhVUPxZ+eK9P/7CBkxvvsBOHqFPEH416726W8tLXXXjKwqgTErrCQ==", "license": "MIT", - "optional": true, "dependencies": { - "is-arrayish": "^0.3.1" + "ip-address": "^10.1.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" } }, "node_modules/sonner": { @@ -6085,10 +10631,19 @@ "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", + "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -6110,6 +10665,18 @@ "dev": true, "license": "MIT" }, + "node_modules/stdin-discarder": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz", + "integrity": "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/stop-iteration-iterator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", @@ -6124,6 +10691,26 @@ "node": ">= 0.4" } }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, "node_modules/string.prototype.includes": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", @@ -6237,11 +10824,39 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/stringify-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/stringify-object/-/stringify-object-5.0.0.tgz", + "integrity": "sha512-zaJYxz2FtcMb4f+g60KsRNFOpVMUyuJgA51Zi5Z1DOTC3S59+OQiVOzE9GZt0x72uBGWKsQIuBKeF9iusmKFsg==", + "license": "BSD-2-Clause", + "dependencies": { + "get-own-enumerable-keys": "^1.0.0", + "is-obj": "^3.0.0", + "is-regexp": "^3.1.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/yeoman/stringify-object?sponsor=1" + } + }, + "node_modules/stringify-object/node_modules/is-obj": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-3.0.0.tgz", + "integrity": "sha512-IlsXEHOjtKhpN8r/tRFj2nDyTmHvcfNeu/nrRIcXE17ROeatXchkojffa1SpdqW4cr/Fj6QkEf/Gn4zf6KKvEQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -6254,12 +10869,23 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", - "dev": true, "license": "MIT", "engines": { "node": ">=4" } }, + "node_modules/strip-final-newline": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-4.0.0.tgz", + "integrity": "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -6322,6 +10948,45 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/swr": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/swr/-/swr-2.5.1.tgz", + "integrity": "sha512-BRw55e8r0B7SpDN20CAzoQAHl7y1yP7/Zt7oqUjMv0vSt2u2Xnkm88Ws+VypbV9BXHQVuSuyVq7zMjO16wSExw==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3", + "use-sync-external-store": "^1.6.0" + }, + "peerDependencies": { + "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/systeminformation": { + "version": "5.33.19", + "resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.33.19.tgz", + "integrity": "sha512-Zm7wGeCG7PqfvzegMJIzrxICopWqYCeLATVmvxOOci1isLcNVKgY4Fr0hhYQzvc7cgtK3377jQVChTFAnznOiA==", + "license": "MIT", + "os": [ + "darwin", + "linux", + "win32", + "freebsd", + "openbsd", + "netbsd", + "sunos", + "android" + ], + "bin": { + "systeminformation": "lib/cli.js" + }, + "engines": { + "node": ">=10.0.0" + }, + "funding": { + "type": "Buy me a coffee", + "url": "https://www.buymeacoffee.com/systeminfo" + } + }, "node_modules/tailwindcss": { "version": "4.1.11", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.11.tgz", @@ -6367,11 +11032,10 @@ "node": ">=18" } }, - "node_modules/text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", - "dev": true, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", "license": "MIT" }, "node_modules/tinyglobby": { @@ -6423,7 +11087,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, "license": "MIT", "dependencies": { "is-number": "^7.0.0" @@ -6457,6 +11120,16 @@ "typescript": ">=4.8.4" } }, + "node_modules/ts-morph": { + "version": "26.0.0", + "resolved": "https://registry.npmjs.org/ts-morph/-/ts-morph-26.0.0.tgz", + "integrity": "sha512-ztMO++owQnz8c/gIENcM9XfCEzgoGphTv+nKpYNM1bgsdOVC/jRZuEBf6N+mLLDNg68Kl+GgUZfOySaRiG1/Ug==", + "license": "MIT", + "dependencies": { + "@ts-morph/common": "~0.27.0", + "code-block-writer": "^13.0.3" + } + }, "node_modules/tsconfig-paths": { "version": "3.15.0", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", @@ -6476,6 +11149,33 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, + "node_modules/tsyringe": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/tsyringe/-/tsyringe-4.10.0.tgz", + "integrity": "sha512-axr3IdNuVIxnaK5XGEUFTu3YmAQ6lllgrvqfEoR16g/HGnYY/6We4oWENtAnzK6/LpJ2ur9PAb80RBt7/U4ugw==", + "license": "MIT", + "dependencies": { + "tslib": "^1.9.3" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/tsyringe/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD" + }, + "node_modules/tw-animate-css": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.4.0.tgz", + "integrity": "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Wombosvideo" + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -6489,19 +11189,6 @@ "node": ">= 0.8.0" } }, - "node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/typed-array-buffer": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", @@ -6581,9 +11268,9 @@ } }, "node_modules/typescript": { - "version": "5.8.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", - "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "version": "5.8.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz", + "integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==", "dev": true, "license": "Apache-2.0", "bin": { @@ -6613,6 +11300,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/undici": { + "version": "7.30.2", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.30.2.tgz", + "integrity": "sha512-mLyPQnHwUxaM/rideoK/+KL8DaAJLRl4aBCfmX444aCSLnIaomqPLtNRe2jdCYcsk5wZzXrDIQC8ZeYNRAYyIA==", + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, "node_modules/undici-types": { "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", @@ -6620,6 +11316,27 @@ "dev": true, "license": "MIT" }, + "node_modules/unicorn-magic": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz", + "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/unrs-resolver": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.11.1.tgz", @@ -6656,9 +11373,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", - "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.4.tgz", + "integrity": "sha512-t8wonIOdQyOjtPd2VktlBu6QjTVTks6v8bSi7kd0r2Vll8TK1JYDZtRRdbmCHOAMphkZgrzK+L+vXK4pyZ8t4A==", "funding": [ { "type": "opencollective", @@ -6695,13 +11412,84 @@ "punycode": "^2.1.0" } }, + "node_modules/use-callback-ref": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "license": "MIT", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sync-external-store": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.7.0.tgz", + "integrity": "sha512-6L+EeigHMQhdaIPNIFUKwfWJSwWFQ8gJbJ2DLOs5sDIegTwR9fRxvnM3uciHKjIZhFz+KAv2emhWMRvDmMcY8A==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, "node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.1.tgz", + "integrity": "sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], "license": "MIT", "bin": { - "uuid": "dist/bin/uuid" + "uuid": "dist/esm/bin/uuid" + } + }, + "node_modules/validate-npm-package-name": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-7.0.2.tgz", + "integrity": "sha512-hVDIBwsRruT73PbK7uP5ebUt+ezEtCmzZz3F59BSr2F6OVFnJ/6h8liuvdLrQ88Xmnk6/+xGGuq+pG9WwTuy3A==", + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/webidl-conversions": { @@ -6730,7 +11518,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, "license": "ISC", "dependencies": { "isexe": "^2.0.0" @@ -6809,6 +11596,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", + "license": "ISC" + }, "node_modules/which-typed-array": { "version": "1.1.19", "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", @@ -6841,11 +11634,40 @@ "node": ">=0.10.0" } }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wsl-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-1.0.1.tgz", + "integrity": "sha512-c16PKLIRxBYpR3jSWsdS7SFp0COm5SOQdiFjY818dYFHJbMGuLBszpF07LBWmOS1uzrHa9VGdynsEgoeKy0oOQ==", + "license": "MIT", + "dependencies": { + "is-wsl": "^3.1.0", + "powershell-utils": "^0.2.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", "license": "ISC" }, "node_modules/yallist": { @@ -6854,6 +11676,93 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "license": "ISC" }, + "node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "license": "MIT", + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "license": "ISC", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/yargs/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yargs/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", @@ -6866,6 +11775,42 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/yocto-spinner": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/yocto-spinner/-/yocto-spinner-1.2.2.tgz", + "integrity": "sha512-DODGl1wJjA/s5pnJFKau9lIYHT81lnhob1i3e1TjxZRxEhWRKl74nTbWE6H5KlkViQQTo/Z29YFdxzTZAMY3ng==", + "license": "MIT", + "dependencies": { + "yoctocolors": "^2.1.1" + }, + "engines": { + "node": ">=18.19" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yoctocolors": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.2.0.tgz", + "integrity": "sha512-xYqdZFUK/VYazNl/oCDYN+3WloWQwMfZxBoiNt6qNyk+xfOdi598muWE42rNZFp1kNOiqW936q5RhUdnpqElSg==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.6.7", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.6.7.tgz", + "integrity": "sha512-g8W2D2kU6a8untc053fX1uVBKzM7yqAxW/3zwk83+6RyF2+SKOuV+c0wBDhUwzV1PaGapmZH+rmcUzPf+3CGwg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/web-app/package.json b/web-app/package.json index c5459b1..d379a2a 100644 --- a/web-app/package.json +++ b/web-app/package.json @@ -6,34 +6,54 @@ "dev": "next dev --turbopack", "build": "next build", "start": "next start", - "lint": "next lint" + "lint": "eslint src", + "typecheck": "tsc --noEmit" }, "dependencies": { + "@simplewebauthn/browser": "^13.3.0", + "@simplewebauthn/server": "^13.3.3", "autoprefixer": "^10.4.21", "bcryptjs": "^3.0.2", + "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "cmdk": "^1.1.1", + "cn": "^0.5.2", "crypto-js": "^4.2.0", "custom-uuid": "^3.1.1", + "date-fns": "^4.4.0", + "geist": "^1.9.2", + "input-otp": "^1.5.0", "lightningcss": "^1.30.1", "lucide-react": "^0.522.0", - "mongoose": "^8.15.1", - "next": "^15.3.5", - "next-auth": "^4.24.11", + "mongoose": "^8.24.8", + "next": "^15.5.29", + "next-auth": "^4.24.15", "next-themes": "^0.4.6", + "otpauth": "^9.6.0", + "qrcode": "^1.5.4", + "radix-ui": "^1.8.1", "rate-limiter-flexible": "^7.1.1", "react": "18.2.0", + "react-day-picker": "^10.0.2", "react-dom": "18.2.0", - "sonner": "^2.0.6" + "server-only": "^0.0.1", + "shadcn": "^4.23.0", + "simple-icons": "^16.36.0", + "sonner": "^2.0.6", + "swr": "^2.5.1", + "tw-animate-css": "^1.4.0", + "zod": "^4.6.7" }, "devDependencies": { "@eslint/eslintrc": "^3", "@tailwindcss/postcss": "^4.1.11", "@types/crypto-js": "^4.2.2", "@types/node": "^20", + "@types/qrcode": "^1.5.6", "@types/react": "^19", "@types/react-dom": "^19", - "eslint": "^8.56.0", - "eslint-config-next": "15.3.2", + "eslint": "^9.39.5", + "eslint-config-next": "^15.5.29", "tailwindcss": "^4.0.0", "typescript": "^5" } diff --git a/web-app/public/assets/envoy-logo.svg b/web-app/public/assets/envoy-logo.svg index 506d815..e0a613a 100644 --- a/web-app/public/assets/envoy-logo.svg +++ b/web-app/public/assets/envoy-logo.svg @@ -1,5 +1,5 @@ - + diff --git a/web-app/public/assets/feature.svg b/web-app/public/assets/feature.svg deleted file mode 100644 index dde0c1d..0000000 --- a/web-app/public/assets/feature.svg +++ /dev/null @@ -1,96 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/web-app/public/assets/google.svg b/web-app/public/assets/google.svg deleted file mode 100644 index 3ffa2aa..0000000 --- a/web-app/public/assets/google.svg +++ /dev/null @@ -1,2 +0,0 @@ - - \ No newline at end of file diff --git a/web-app/public/assets/heart.svg b/web-app/public/assets/heart.svg deleted file mode 100644 index 081255d..0000000 --- a/web-app/public/assets/heart.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/web-app/public/assets/hero-img.svg b/web-app/public/assets/hero-img.svg deleted file mode 100644 index 0e500b1..0000000 --- a/web-app/public/assets/hero-img.svg +++ /dev/null @@ -1,278 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/web-app/src/app/(app)/account/page.tsx b/web-app/src/app/(app)/account/page.tsx new file mode 100644 index 0000000..649687e --- /dev/null +++ b/web-app/src/app/(app)/account/page.tsx @@ -0,0 +1,207 @@ +"use client"; + +import { useEffect, useState } from "react"; +import Link from "next/link"; +import useSWR from "swr"; +import { signOut } from "next-auth/react"; +import { toast } from "sonner"; +import { Button } from "@/components/ui/button"; +import { PageContainer } from "@/components/app/app-shell"; +import { TwoFactorSettings } from "@/components/app/two-factor-settings"; +import { LoadingPage } from "@/components/app/states"; +import { api, fetcher } from "@/lib/api-client"; +import { formatDate } from "@/lib/format"; +import { FormField, PageHeader, Section, ToneBadge, UserAvatar } from "@/components/common"; +import { ConfirmDialog } from "@/components/confirm-dialog"; +import { Input } from "@/components/ui/input"; +import { Badge } from "@/components/ui/badge"; + +type Me = { + user: { + id: string; + name: string; + email: string; + image: string | null; + username?: string; + provider: "credentials" | "google"; + emailVerified: boolean; + totpEnabled: boolean; + hasPassword: boolean; + backupCodesRemaining: number; + createdAt: string; + apiTokens: number; + }; +}; + +export default function AccountPage() { + const { data, mutate } = useSWR("/api/me", fetcher); + const [name, setName] = useState(""); + const [savingName, setSavingName] = useState(false); + const [signOutAllOpen, setSignOutAllOpen] = useState(false); + const [pw, setPw] = useState({ current: "", next: "" }); + const [pwBusy, setPwBusy] = useState(false); + const [pwError, setPwError] = useState(null); + + useEffect(() => { + if (data) setName(data.user.name); + }, [data]); + + if (!data) return ; + const { user } = data; + + return ( + + + +
+
+ +

+ {user.image ? "Your photo comes from your Google account." : "Sign in with Google to use your Google profile photo."} +

+
+
{ + e.preventDefault(); + setSavingName(true); + try { + await api("/api/me", { method: "PATCH", body: { name } }); + toast.success("Profile updated. It shows everywhere after your next sign-in."); + mutate(); + } catch (err: any) { + toast.error(err.message); + } finally { + setSavingName(false); + } + }} + > + + setName(e.target.value)} maxLength={64} required /> + + + + +
+ +
+
+ + +
+ mutate()} /> +
+ +
+
{ + e.preventDefault(); + setPwBusy(true); + setPwError(null); + try { + const res = await api<{ signedOut: boolean }>("/api/account/password", { + body: user.hasPassword ? { currentPassword: pw.current, newPassword: pw.next } : { newPassword: pw.next }, + }); + if (res.signedOut) { + toast.success("Password changed. Sign in again with your new password."); + await signOut({ callbackUrl: "/login" }); + return; + } + toast.success(`Password set. You can now sign in with ${user.email} and this password.`); + setPw({ current: "", next: "" }); + setPwBusy(false); + mutate(); + } catch (err: any) { + setPwError(err.message); + setPwBusy(false); + } + }} + > + {/* Lets password managers save the new password against the right account. */} + + {user.hasPassword && ( + + setPw({ ...pw, current: e.target.value })} + /> + + )} + + setPw({ ...pw, next: e.target.value })} + /> + +
+ +
+
+
+ +
+
+

+ {user.apiTokens} active token{user.apiTokens === 1 ? "" : "s"} +

+ +
+
+ +
+
+ +

Account created {formatDate(user.createdAt)}.

+
+
+ + { + await api("/api/account/sessions", { method: "DELETE" }); + await signOut({ callbackUrl: "/login" }); + }} + /> + + ); +} diff --git a/web-app/src/app/(app)/account/tokens/page.tsx b/web-app/src/app/(app)/account/tokens/page.tsx new file mode 100644 index 0000000..ced9341 --- /dev/null +++ b/web-app/src/app/(app)/account/tokens/page.tsx @@ -0,0 +1,202 @@ +"use client"; + +import { useState } from "react"; +import { toast } from "sonner"; +import { KeyRound, Plus } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"; +import { PageContainer } from "@/components/app/app-shell"; +import { api } from "@/lib/api-client"; +import { ExpiryField, customExpiryISO, type Expiry } from "@/components/expiry-field"; +import { usePaginated } from "@/lib/use-paginated"; +import { formatDate, timeAgo } from "@/lib/format"; +import { FormField, CopyButton, EmptyState, LoadMore, PageHeader, copyText } from "@/components/common"; +import { ConfirmDialog } from "@/components/confirm-dialog"; +import { DialogCancel, DialogSubmit } from "@/components/dialog-actions"; +import { Input } from "@/components/ui/input"; +import { Skeleton } from "@/components/ui/skeleton"; + +type Token = { id: string; name: string; prefix: string; createdAt: string; lastUsedAt: string | null; expiresAt: string | null }; + +function CreateTokenDialog({ open, onOpenChange, onCreated }: { open: boolean; onOpenChange: (o: boolean) => void; onCreated: () => void }) { + const [name, setName] = useState(""); + const [expiry, setExpiry] = useState({ preset: "90", date: "" }); + const [busy, setBusy] = useState(false); + const [error, setError] = useState(null); + const [token, setToken] = useState(null); + + const close = (o: boolean) => { + onOpenChange(o); + if (!o) { + setName(""); + setToken(null); + setError(null); + } + }; + + return ( + + + + {token ? "Copy your token" : "Create API token"} + { + token + ? "This is the only time it's shown. Store it in your password manager or CI secrets." + : "The token can read every project you can read. If you lose access to a team, so does the token." + } + + {token ? ( +
{ + // The token is never shown again, so Enter copies it before closing. + e.preventDefault(); + await copyText(token, "Token copied"); + close(false); + }} + > +
+ {token} + +
+

+ Try it: export ENVOY_TOKEN=… then use the + command from a project's “Use from CLI” button. +

+ + Copy and close + +
+ ) : ( +
{ + e.preventDefault(); + setBusy(true); + setError(null); + try { + const res = await api<{ token: string }>("/api/tokens", { + body: + expiry.preset === "custom" + ? { name, expiresAt: customExpiryISO(expiry.date) } + : { name, expiresInDays: expiry.preset === "never" ? null : Number(expiry.preset) }, + }); + setToken(res.token); + onCreated(); + } catch (err: any) { + setError(err.message); + } finally { + setBusy(false); + } + }} + > + + setName(e.target.value)} /> + + + + close(false)} /> + + Create token + + + + )} +
+
+ ); +} + +export default function TokensPage() { + const list = usePaginated("/api/tokens", "tokens"); + const data = list.isLoading ? undefined : { tokens: list.items }; + const mutate = list.mutate; + const [createOpen, setCreateOpen] = useState(false); + const [revoking, setRevoking] = useState(null); + + return ( + + setCreateOpen(true)}> + New token + + } + /> + + {!data ? ( + + ) : data.tokens.length === 0 ? ( + } + title="No tokens yet" + action={ + + } + > + Create one per machine or pipeline, so you can revoke it without affecting anything else. + + ) : ( +
    + {data.tokens.map((t) => { + const expired = t.expiresAt && new Date(t.expiresAt).getTime() < Date.now(); + return ( +
  • + +
    +

    {t.name}

    +

    + {t.prefix}…, created {timeAgo(t.createdAt)}, last used{" "} + {timeAgo(t.lastUsedAt)} +

    +
    + + {t.expiresAt ? (expired ? "Expired" : `Expires ${timeAgo(t.expiresAt)}`) : "No expiry"} + + +
  • + ); + })} +
+ )} + + + mutate()} /> + !o && setRevoking(null)} + title={`Revoke ${revoking?.name}?`} + description="Anything using this token stops working immediately." + confirmText={revoking?.name} + actionLabel="Revoke token" + onConfirm={async () => { + try { + await api(`/api/tokens/${revoking!.id}`, { method: "DELETE" }); + toast.success("Token revoked"); + mutate(); + } catch (e: any) { + toast.error(e.message); + } + }} + /> +
+ ); +} diff --git a/web-app/src/app/(app)/error.tsx b/web-app/src/app/(app)/error.tsx new file mode 100644 index 0000000..518786f --- /dev/null +++ b/web-app/src/app/(app)/error.tsx @@ -0,0 +1,18 @@ +"use client"; + +import { useEffect } from "react"; +import { ErrorState } from "@/components/app/states"; + +/** Unexpected errors inside the signed-in app: shown in place of the page, keeping the sidebar and top bar. */ +export default function AppError({ error, reset }: { error: Error & { digest?: string }; reset: () => void }) { + useEffect(() => { + console.error(error); + }, [error]); + + return ( + + This page hit an unexpected error. Nothing was changed. Try again, and if it + keeps happening, let us know. + + ); +} diff --git a/web-app/src/app/(app)/layout.tsx b/web-app/src/app/(app)/layout.tsx new file mode 100644 index 0000000..e405797 --- /dev/null +++ b/web-app/src/app/(app)/layout.tsx @@ -0,0 +1,5 @@ +import { AppShell } from "@/components/app/app-shell"; + +export default function AppLayout({ children }: { children: React.ReactNode }) { + return {children}; +} diff --git a/web-app/src/app/(app)/o/[org]/page.tsx b/web-app/src/app/(app)/o/[org]/page.tsx new file mode 100644 index 0000000..84bedf5 --- /dev/null +++ b/web-app/src/app/(app)/o/[org]/page.tsx @@ -0,0 +1,129 @@ +"use client"; + +import { useState } from "react"; +import Link from "next/link"; +import { FolderPlus, Plus, UserPlus, Users } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { PageContainer } from "@/components/app/app-shell"; +import { CreateProjectDialog, CreateTeamDialog } from "@/components/app/dialogs"; +import { SetupChecklist } from "@/components/app/setup-checklist"; +import { ROLE_LABEL, useOrg } from "@/components/app/org-context"; +import { LoadError, LoadingPage, LowBackupCodesBanner } from "@/components/app/states"; +import { timeAgo } from "@/lib/format"; +import { EmptyState, PageHeader, ToneBadge, ToneTile } from "@/components/common"; +import { Badge } from "@/components/ui/badge"; +import { roleTone } from "@/lib/tones"; + +export default function OrgOverviewPage() { + const { data, error, slug, isAdmin, mutate } = useOrg(); + const [createTeam, setCreateTeam] = useState(false); + const [projectFor, setProjectFor] = useState<{ slug: string; name: string } | null>(null); + + if (error) return mutate()} />; + if (!data) return ; + + const { org, teams } = data; + const projectCount = teams.reduce((n, t) => n + t.projects.length, 0); + + return ( + + + + {org.name} + {ROLE_LABEL[org.role]} + + } + description={`${teams.length} ${isAdmin ? "" : "of your "}team${teams.length === 1 ? "" : "s"}, ${projectCount} project${projectCount === 1 ? "" : "s"}, ${org.memberCount} member${org.memberCount === 1 ? "" : "s"}.`} + actions={ + isAdmin && ( + <> + + + + ) + } + /> + + {isAdmin && slug && ( + 0} + onCreateTeam={() => setCreateTeam(true)} + onCreateProject={setProjectFor} + /> + )} + + {teams.length === 0 ? ( + !isAdmin && ( + } title="You're not on a team yet"> + Ask an owner or admin of {org.name} to add you to a team. Its projects will appear here. + + ) + ) : ( +
+ {teams.map((team) => ( +
+
+
+ +

+ + {team.name} + +

+ + {team.memberCount} member{team.memberCount === 1 ? "" : "s"} + + {team.access === "view" && Read-only} +
+ {team.access !== "view" && ( + + )} +
+ {team.projects.length === 0 ? ( +

+ No projects in {team.name} yet. +

+ ) : ( +
    + {team.projects.map((p) => ( +
  • + + {p.slug} + Updated {timeAgo(p.updatedAt)} + +
  • + ))} +
+ )} +
+ ))} +
+ )} + + + {projectFor && ( + !o && setProjectFor(null)} + teamSlug={projectFor.slug} + teamName={projectFor.name} + /> + )} +
+ ); +} diff --git a/web-app/src/app/(app)/o/[org]/settings/audit/page.tsx b/web-app/src/app/(app)/o/[org]/settings/audit/page.tsx new file mode 100644 index 0000000..f63dc2e --- /dev/null +++ b/web-app/src/app/(app)/o/[org]/settings/audit/page.tsx @@ -0,0 +1,250 @@ +"use client"; + +import { useEffect, useState } from "react"; +import useSWRInfinite from "swr/infinite"; +import { ChevronRight } from "lucide-react"; +import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; +import { Button } from "@/components/ui/button"; +import { useOrg } from "@/components/app/org-context"; +import { NotFoundState } from "@/components/app/states"; +import { fetcher } from "@/lib/api-client"; +import { cn } from "@/lib/utils"; +import { formatDate, timeAgo } from "@/lib/format"; +import { UserAvatar, EmptyState, SearchInput, ToneDot } from "@/components/common"; +import type { Tone } from "@/lib/tones"; +import { Skeleton } from "@/components/ui/skeleton"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; + +type Entry = { + id: string; + action: string; + actor: { id: string; name: string; email: string } | null; + target: { type: string; id?: string; name?: string } | null; + meta: Record | null; + ip: string | null; + createdAt: string; +}; +type Page = { entries: Entry[]; nextCursor: string | null }; + +const ACTIONS: Record = { + "org.create": "created the organization", + "org.update": "updated organization settings", + "org.2fa_required": "made two-factor authentication required in", + "org.2fa_optional": "made two-factor authentication optional in", + "org.transfer": "transferred ownership to", + "member.join": "joined the organization", + "member.role_change": "changed the role of", + "member.remove": "removed", + "member.leave": "left the organization", + "member.2fa_reset": "reset two-factor authentication for", + "team.create": "created team", + "team.update": "updated team", + "team.delete": "deleted team", + "team.member_add": "added a member to", + "team.member_role_change": "changed team access in", + "team.member_remove": "removed a member from", + "project.create": "created project", + "project.update": "updated project", + "project.delete": "deleted project", + "project.access_change": "changed who can open", + "project.move": "moved project", + "env.create": "created environment", + "env.update": "renamed environment", + "env.delete": "deleted environment", + "vars.update": "changed variables in", + "invite.create": "invited", + "invite.revoke": "revoked the invitation for", + "invite.accept": "accepted an invitation to", +}; + +const CATEGORY_TONE: Record = { + vars: "mint", + member: "sky", + team: "lavender", + project: "peach", + env: "lemon", + invite: "rose", + org: "lavender", +}; + +const FILTERS = [ + { value: "", label: "All activity" }, + { value: "vars.", label: "Variable changes" }, + { value: "member.", label: "Members" }, + { value: "team.", label: "Teams" }, + { value: "project.", label: "Projects" }, + { value: "env.", label: "Environments" }, + { value: "invite.", label: "Invitations" }, + { value: "org.", label: "Organization" }, +]; + +function summarize(e: Entry) { + const m = e.meta ?? {}; + if (e.action === "vars.update") { + const parts = [ + m.added?.length && `${m.added.length} added`, + m.changed?.length && `${m.changed.length} changed`, + m.removed?.length && `${m.removed.length} removed`, + ].filter(Boolean); + return parts.join(", "); + } + if (e.action === "member.role_change" || e.action === "team.member_role_change") { + return `${m.user ? `${m.user}: ` : ""}${m.from} to ${m.to}`; + } + if (e.action === "team.member_add") return `${m.user} as ${m.role}`; + if (e.action === "team.member_remove") return m.user; + if (e.action === "member.join") return [m.orgRole, m.team && `${m.teamRole} on ${m.team}`].filter(Boolean).join(", "); + return null; +} + +function KeyList({ label, keys }: { label: string; keys?: string[] }) { + if (!keys?.length) return null; + return ( +
+
{label}
+
+ {keys.map((k) => ( + + {k} + + ))} +
+
+ ); +} + +export default function AuditPage() { + const { slug, isAdmin } = useOrg(); + const [action, setAction] = useState(""); + const [q, setQ] = useState(""); + const [query, setQuery] = useState(""); + const [open, setOpen] = useState(null); + + useEffect(() => { + const t = setTimeout(() => setQuery(q), 300); + return () => clearTimeout(t); + }, [q]); + + const getKey = (index: number, prev: Page | null) => { + if (!slug || !isAdmin || (prev && !prev.nextCursor)) return null; + const p = new URLSearchParams(); + if (action) p.set("action", action); + if (query) p.set("q", query); + if (prev?.nextCursor) p.set("cursor", prev.nextCursor); + return `/api/orgs/${slug}/audit?${p}`; + }; + const { data, size, setSize, isValidating } = useSWRInfinite(getKey, fetcher); + + if (!isAdmin) return ; + + const entries = data?.flatMap((p) => p.entries) ?? []; + const hasMore = !!data?.[data.length - 1]?.nextCursor; + + return ( +
+

+ Every change to members, teams, projects, environments and variables. Variable entries record which keys + changed, never their values. +

+
+ + +
+ + {!data ? ( +
+ {[0, 1, 2, 3, 4].map((i) => ( + + ))} +
+ ) : entries.length === 0 ? ( + {action || query ? "Try a different filter." : "Changes in this organization will be listed here."} + ) : ( +
    + {entries.map((e) => { + const expanded = open === e.id; + const detail = summarize(e); + return ( + setOpen(o ? e.id : null)}> +
  1. + + + + + +
    +

    + {e.actor?.name ?? "System"}{" "} + {ACTIONS[e.action] ?? e.action}{" "} + {e.target?.name && e.action !== "member.join" && e.action !== "member.leave" && ( + + {e.target.name} + + )} +

    + {detail &&

    {detail}

    } +
    + + +
    + +
    +
    +
    When
    +
    {formatDate(e.createdAt)}
    +
    +
    +
    Who
    +
    + {e.actor?.email ?? "Unknown"} + {e.ip && from {e.ip}} +
    +
    + + + + {e.action !== "vars.update" && e.meta && ( +
    +
    Details
    +
    +
    +                                                        {JSON.stringify(e.meta, null, 2)}
    +                                                    
    +
    +
    + )} +
    +
    +
  2. +
    + ); + })} +
+ )} + + {hasMore && ( +
+ +
+ )} +
+ ); +} diff --git a/web-app/src/app/(app)/o/[org]/settings/invites/page.tsx b/web-app/src/app/(app)/o/[org]/settings/invites/page.tsx new file mode 100644 index 0000000..df1aeac --- /dev/null +++ b/web-app/src/app/(app)/o/[org]/settings/invites/page.tsx @@ -0,0 +1,270 @@ +"use client"; + +import { useState } from "react"; +import { toast } from "sonner"; +import { Copy, Link2, Mail, X } from "lucide-react"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { ButtonGroup } from "@/components/ui/button-group"; +import { Checkbox } from "@/components/ui/checkbox"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { Skeleton } from "@/components/ui/skeleton"; +import { copyText, EmptyState, FormField, LoadMore } from "@/components/common"; +import { formatInviteCode } from "@/lib/invite-code"; +import { ExpiryField, customExpiryISO, type Expiry } from "@/components/expiry-field"; +import { ROLE_LABEL, useOrg } from "@/components/app/org-context"; +import { NotFoundState } from "@/components/app/states"; +import { ConfirmDialog } from "@/components/confirm-dialog"; +import { api } from "@/lib/api-client"; +import { usePaginated } from "@/lib/use-paginated"; +import { formatDate, timeAgo } from "@/lib/format"; + +type Invite = { + id: string; + kind: "email" | "link"; + email: string | null; + orgRole: "admin" | "member"; + team: { name: string; slug: string } | null; + teamRole: "editor" | "viewer" | null; + expiresAt: string; + maxUses: number | null; + uses: number; + createdBy: string | null; + createdAt: string; + status: "active" | "revoked" | "expired" | "accepted" | "used_up"; +}; + +const STATUS_LABEL: Record = { + active: "Active", + revoked: "Revoked", + expired: "Expired", + accepted: "Accepted", + used_up: "Used up", +}; + +const NO_TEAM = "__none"; + +export default function InvitesPage() { + const { slug, data: orgData, isAdmin, isOwner } = useOrg(); + const key = `/api/orgs/${slug}/invites`; + const list = usePaginated(isAdmin ? key : null, "invites"); + const data = list.isLoading ? undefined : list.first; + const mutate = list.mutate; + + const [orgRole, setOrgRole] = useState<"member" | "admin">("member"); + const [teamSlug, setTeamSlug] = useState(NO_TEAM); + const [teamRole, setTeamRole] = useState<"viewer" | "editor">("viewer"); + const [expiry, setExpiry] = useState({ preset: "7", date: "" }); + const [maxUses, setMaxUses] = useState(""); + const [busy, setBusy] = useState(false); + const [error, setError] = useState(null); + const [createdCode, setCreatedCode] = useState(null); + const [showAll, setShowAll] = useState(false); + const [revoking, setRevoking] = useState(null); + + if (!isAdmin) return ; + + const teams = orgData?.teams ?? []; + const invites = list.items.filter((i) => showAll || i.status === "active"); + const hasTeam = teamSlug !== NO_TEAM; + + async function create(e: React.FormEvent) { + e.preventDefault(); + setBusy(true); + setError(null); + setCreatedCode(null); + try { + const res = await api<{ token?: string }>(key, { + body: { + orgRole, + teamSlug: hasTeam ? teamSlug : undefined, + teamRole: hasTeam ? teamRole : undefined, + ...(expiry.preset === "custom" ? { expiresAt: customExpiryISO(expiry.date) } : { expiresInDays: Number(expiry.preset) }), + maxUses: maxUses ? Number(maxUses) : undefined, + }, + }); + if (res.token) setCreatedCode(res.token); + mutate(); + } catch (err: any) { + setError(err.message); + } finally { + setBusy(false); + } + } + + return ( +
+
+

Create an invite

+

+ You get a 10-character code to share, plus a link that + contains it. Send it privately: anyone who has it can join + until it expires or runs out of uses. +

+ +
+ + + +
+ + + + + + +
+
+ ({ value: String(d), label: `${d} day${d === 1 ? "" : "s"}` }))} + /> + + setMaxUses(e.target.value)} + /> + +
+ {error && ( +

+ {error} +

+ )} + +
+ + {createdCode && ( +
+

Share this invite code

+

+ Copy it now: for security it won't be shown again. +

+

{formatInviteCode(createdCode)}

+ + + + +
+ )} +
+ +
+
+

{showAll ? "All invites" : "Active invites"}

+
+ setShowAll(v === true)} /> + +
+
+ {!data ? ( + + ) : invites.length === 0 ? ( + Invites you create show up here until they expire, run out of uses or you revoke them. + ) : ( +
    + {invites.map((inv) => ( +
  • + {inv.kind === "email" ? : } +
    +

    + {inv.kind === "email" + ? `Email invite for ${inv.email}` // created before invitations became links-only + : `Invite${inv.maxUses ? `, ${inv.uses} of ${inv.maxUses} used` : `, used ${inv.uses} time${inv.uses === 1 ? "" : "s"}`}`} +

    +

    + {ROLE_LABEL[inv.orgRole]} + {inv.team ? ` and ${ROLE_LABEL[inv.teamRole ?? "viewer"].toLowerCase()} on ${inv.team.name}` : ""}.{" "} + {inv.status === "active" ? ( + Expires {timeAgo(inv.expiresAt)} + ) : ( + Created {timeAgo(inv.createdAt)} + )} + {inv.createdBy ? ` by ${inv.createdBy}` : ""} +

    +
    + {inv.status === "active" ? ( + + ) : ( + {STATUS_LABEL[inv.status]} + )} +
  • + ))} +
+ )} + +
+ !o && setRevoking(null)} + title="Revoke this invite?" + description="Its code and link stop working right away. People who already joined keep their access." + confirmText="revoke" + actionLabel="Revoke invite" + onConfirm={async () => { + try { + await api(`${key}/${revoking!.id}`, { method: "DELETE" }); + toast.success("Invite revoked"); + mutate(); + } catch (e: any) { + toast.error(e.message); + } + }} + /> +
+ ); +} diff --git a/web-app/src/app/(app)/o/[org]/settings/layout.tsx b/web-app/src/app/(app)/o/[org]/settings/layout.tsx new file mode 100644 index 0000000..7da477f --- /dev/null +++ b/web-app/src/app/(app)/o/[org]/settings/layout.tsx @@ -0,0 +1,54 @@ +"use client"; + +import Link from "next/link"; +import { usePathname } from "next/navigation"; +import { PageContainer } from "@/components/app/app-shell"; +import { PageHeader } from "@/components/common"; +import { useOrg } from "@/components/app/org-context"; +import { LoadError, LoadingPage } from "@/components/app/states"; +import { cn } from "@/lib/utils"; + +export default function SettingsLayout({ children }: { children: React.ReactNode }) { + const { slug, data, error, isAdmin, mutate } = useOrg(); + const pathname = usePathname(); + + if (error) return mutate()} />; + if (!data) return ; + + const base = `/o/${slug}/settings`; + const tabs = [ + { href: base, label: "General" }, + { href: `${base}/members`, label: "Members" }, + ...(isAdmin + ? [ + { href: `${base}/invites`, label: "Invitations" }, + { href: `${base}/audit`, label: "Audit log" }, + ] + : []), + ]; + + return ( + + + + {children} + + ); +} diff --git a/web-app/src/app/(app)/o/[org]/settings/members/page.tsx b/web-app/src/app/(app)/o/[org]/settings/members/page.tsx new file mode 100644 index 0000000..20c2ea2 --- /dev/null +++ b/web-app/src/app/(app)/o/[org]/settings/members/page.tsx @@ -0,0 +1,276 @@ +"use client"; + +import { useEffect, useState } from "react"; +import Link from "next/link"; +import { toast } from "sonner"; +import { Check, Crown, MoreHorizontal, ShieldCheck, ShieldOff, Trash2, UserPlus, UserRound } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { ROLE_LABEL, useOrg } from "@/components/app/org-context"; +import useSWR from "swr"; +import { api, fetcher } from "@/lib/api-client"; +import { usePaginated } from "@/lib/use-paginated"; +import { formatDate, timeAgo } from "@/lib/format"; +import { LoadMore, UserAvatar, SearchInput, ToneBadge } from "@/components/common"; +import { ConfirmDialog } from "@/components/confirm-dialog"; +import { Badge } from "@/components/ui/badge"; +import { Skeleton } from "@/components/ui/skeleton"; +import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem } from "@/components/ui/dropdown-menu"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { roleTone, TONE_BADGE } from "@/lib/tones"; +import { cn } from "@/lib/utils"; + +type Member = { + userId: string; + name: string; + email: string; + image: string | null; + provider: "credentials" | "google"; + totpEnabled: boolean; + role: "owner" | "admin" | "member"; + joinedAt: string; + teams: { name: string; slug: string; role: string }[]; + isMe: boolean; +}; + +type Pending = { kind: "remove" | "transfer" | "reset2fa"; member: Member } | null; + +const ROLE_CARDS: { role: "owner" | "admin" | "member"; icon: React.ElementType; summary: string; can: string[] }[] = [ + { + role: "owner", + icon: Crown, + summary: "One per organization, with full control.", + can: ["Everything an admin can do", "Delete the organization", "Transfer ownership", "Reset a member's two-factor"], + }, + { + role: "admin", + icon: ShieldCheck, + summary: "Runs the organization day to day.", + can: ["Create teams and add people to them", "Invite and remove members", "Edit every project", "Read the audit log"], + }, + { + role: "member", + icon: UserRound, + summary: "Works inside the teams they're added to.", + can: ["Sees only their own teams", "Editor or viewer per team", "Uses API tokens with the same access"], + }, +]; + +export default function MembersPage() { + const { slug, isAdmin, isOwner, mutate: mutateOrg } = useOrg(); + const { data: security } = useSWR<{ require2fa: boolean }>(slug ? `/api/orgs/${slug}/security` : null, fetcher); + const require2fa = !!security?.require2fa; + const key = `/api/orgs/${slug}/members`; + const [query, setQuery] = useState(""); + const [debounced, setDebounced] = useState(""); + const [pending, setPending] = useState(null); + + // Search runs on the server (it covers every member, not just loaded pages); wait for typing to pause. + useEffect(() => { + const t = setTimeout(() => setDebounced(query.trim()), 250); + return () => clearTimeout(t); + }, [query]); + + const list = usePaginated(slug ? (debounced ? `${key}?q=${encodeURIComponent(debounced)}` : key) : null, "members"); + const members = list.items; + const data = list.isLoading ? undefined : list.first; + const mutate = list.mutate; + + async function changeRole(m: Member, role: string) { + try { + await api(`${key}/${m.userId}`, { method: "PATCH", body: { role } }); + toast.success(`${m.name} is now ${role === "admin" ? "an admin" : "a member"}`); + mutate(); + mutateOrg(); + } catch (e: any) { + toast.error(e.message); + } + } + + // Admins can change members; only the owner can change admins. + const canManage = (m: Member) => !m.isMe && m.role !== "owner" && (isOwner || (isAdmin && m.role === "member")); + + return ( +
+
+ + {isAdmin && ( + + )} +
+ + {!data ? ( +
+ {[0, 1, 2].map((i) => ( + + ))} +
+ ) : ( +
    + {members.map((m) => ( +
  • + +
    +

    + {m.name} + {m.isMe && (you)} +

    +

    + {m.email} + {m.teams.length > 0 && , {m.teams.map((t) => t.name).join(", ")}} +

    +
    +
    + {!m.totpEnabled && + (require2fa ? ( + + Needs 2FA + + ) : ( + + No 2FA + + ))} + + Joined {timeAgo(m.joinedAt)} + + {canManage(m) ? ( + + ) : ( + + {ROLE_LABEL[m.role]} + + )} + {(canManage(m) || (isOwner && !m.isMe)) && ( + + + + + + {isOwner && m.role === "admin" && ( + setPending({ kind: "transfer", member: m })}> + Transfer ownership + + )} + {isOwner && m.totpEnabled && ( + setPending({ kind: "reset2fa", member: m })}> + Reset two-factor authentication + + )} + {canManage(m) && ( + setPending({ kind: "remove", member: m })}> + Remove from organization + + )} + + + )} +
    +
  • + ))} + {members.length === 0 &&
  • No members match “{query}”.
  • } +
+ )} + + +
+

Roles

+
    + {ROLE_CARDS.map((r) => ( +
  • +
    + + + +

    {ROLE_LABEL[r.role]}

    +
    +

    {r.summary}

    +
      + {r.can.map((c) => ( +
    • + + {c} +
    • + ))} +
    +
  • + ))} +
+
+ + !o && setPending(null)} + title={`Remove ${pending?.member.name}?`} + description="They'll lose access to every team and project in this organization right away, including through their API tokens." + confirmText={pending?.member.email} + actionLabel="Remove member" + onConfirm={async () => { + try { + await api(`${key}/${pending!.member.userId}`, { method: "DELETE" }); + toast.success(`Removed ${pending!.member.name}`); + mutate(); + mutateOrg(); + } catch (e: any) { + toast.error(e.message); + } + }} + /> + !o && setPending(null)} + title={`Make ${pending?.member.name} the owner?`} + description="You'll become an admin. Only the new owner can delete the organization or transfer ownership again." + actionLabel="Transfer ownership" + danger={false} + onConfirm={async () => { + try { + await api(`/api/orgs/${slug}/transfer`, { body: { userId: pending!.member.userId } }); + toast.success("Ownership transferred"); + mutate(); + mutateOrg(); + } catch (e: any) { + toast.error(e.message); + } + }} + /> + !o && setPending(null)} + title={`Reset 2FA for ${pending?.member.name}?`} + description={ + <> + Use this only if they've lost their authenticator and backup codes, and you've confirmed + it's really them. They'll be signed out everywhere and must set up 2FA again after signing in + with their password or Google. This is recorded in the audit log. + + } + confirmText={pending?.member.email} + actionLabel="Reset 2FA" + onConfirm={async () => { + try { + const res = await api<{ message: string }>(`${key}/${pending!.member.userId}/reset-2fa`, { method: "POST" }); + toast.success(res.message); + mutate(); + } catch (e: any) { + toast.error(e.message); + } + }} + /> +
+ ); +} + diff --git a/web-app/src/app/(app)/o/[org]/settings/page.tsx b/web-app/src/app/(app)/o/[org]/settings/page.tsx new file mode 100644 index 0000000..ecaaa89 --- /dev/null +++ b/web-app/src/app/(app)/o/[org]/settings/page.tsx @@ -0,0 +1,161 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { useRouter } from "next/navigation"; +import { toast } from "sonner"; +import { Button } from "@/components/ui/button"; +import { InputGroup, InputGroupAddon, InputGroupInput, InputGroupText } from "@/components/ui/input-group"; +import { useOrg } from "@/components/app/org-context"; +import { api } from "@/lib/api-client"; +import { FormField, Section } from "@/components/common"; +import { ConfirmDialog } from "@/components/confirm-dialog"; +import { Input } from "@/components/ui/input"; +import { OrgSecurity } from "@/components/app/org-security"; + +export default function GeneralSettingsPage() { + const { data, slug, mutate, isAdmin, isOwner } = useOrg(); + const router = useRouter(); + const [name, setName] = useState(""); + const [newSlug, setNewSlug] = useState(""); + const [busy, setBusy] = useState(false); + const [error, setError] = useState(null); + const [leaveOpen, setLeaveOpen] = useState(false); + const [deleteOpen, setDeleteOpen] = useState(false); + + useEffect(() => { + if (data) { + setName(data.org.name); + setNewSlug(data.org.slug); + } + }, [data]); + + if (!data) return null; + const changed = name !== data.org.name || newSlug !== data.org.slug; + + return ( +
+
+
{ + e.preventDefault(); + setBusy(true); + setError(null); + try { + const res = await api<{ org: { slug: string } }>(`/api/orgs/${slug}`, { + method: "PATCH", + body: { name, slug: newSlug !== data.org.slug ? newSlug : undefined }, + }); + toast.success("Organization updated"); + if (res.org.slug !== slug) { + await api("/api/me", { method: "PATCH", body: { lastOrgSlug: res.org.slug } }); + router.replace(`/o/${res.org.slug}/settings`); + } else mutate(); + } catch (err: any) { + setError(err.message); + } finally { + setBusy(false); + } + }} + > + + setName(e.target.value)} disabled={!isAdmin} maxLength={64} required /> + + + + + /o/ + + setNewSlug(e.target.value.toLowerCase())} + disabled={!isAdmin} + pattern="[a-z0-9][a-z0-9\-]{0,46}[a-z0-9]?" + className="font-mono text-[13px]" + /> + + + {isAdmin && ( +
+ +
+ )} +
+
+ +
+ +
+ + {!isOwner && ( +
+ +
+ )} + + {isOwner && ( +
+
+

+ To leave instead, first transfer ownership to an admin from the Members tab. +

+ +
+
+ )} + + { + try { + await api(`/api/orgs/${slug}/members/me`, { method: "DELETE" }); + toast.success(`You left ${data.org.name}`); + window.location.assign("/dashboard"); + } catch (e: any) { + toast.error(e.message); + } + }} + /> + { + try { + await api(`/api/orgs/${slug}`, { method: "DELETE", body: { confirm: typed } }); + toast.success("Organization deleted"); + window.location.assign("/dashboard"); + } catch (e: any) { + toast.error(e.message); + } + }} + /> +
+ ); +} diff --git a/web-app/src/app/(app)/o/[org]/t/[team]/p/[project]/page.tsx b/web-app/src/app/(app)/o/[org]/t/[team]/p/[project]/page.tsx new file mode 100644 index 0000000..038e9e1 --- /dev/null +++ b/web-app/src/app/(app)/o/[org]/t/[team]/p/[project]/page.tsx @@ -0,0 +1,425 @@ +"use client"; + +import { Suspense, useCallback, useState } from "react"; +import Link from "next/link"; +import { useParams, useRouter, useSearchParams } from "next/navigation"; +import useSWR from "swr"; +import { toast } from "sonner"; +import { ArrowRightLeft, Lock, MoreHorizontal, Pencil, Plus, Terminal, Trash2 } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { ButtonGroup } from "@/components/ui/button-group"; +import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog"; +import { PageContainer } from "@/components/app/app-shell"; +import { QuickCreateDialog } from "@/components/quick-create-dialog"; +import { EnvEditor } from "@/components/app/env-editor"; +import { ProjectAccessDialog, RestrictedLock } from "@/components/app/project-access-dialog"; +import { MoveProjectDialog } from "@/components/app/move-project-dialog"; +import { useOrg } from "@/components/app/org-context"; +import { LoadError, LoadingPage } from "@/components/app/states"; +import { api, fetcher } from "@/lib/api-client"; +import { timeAgo } from "@/lib/format"; +import { CopyButton, PageHeader, ToneBadge, ToneDot } from "@/components/common"; +import { ConfirmDialog } from "@/components/confirm-dialog"; +import { Badge } from "@/components/ui/badge"; +import { Skeleton } from "@/components/ui/skeleton"; +import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; +import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator } from "@/components/ui/dropdown-menu"; +import { envTone } from "@/lib/tones"; + +type ProjectData = { + project: { id: string; name: string; slug: string; description: string; restricted?: boolean }; + team: { name: string; slug: string }; + org: { name: string; slug: string }; + access: "manage" | "edit" | "view"; + environments: { id: string; name: string; slug: string; varCount: number; updatedAt: string }[]; +}; + +type EnvData = { + environment: { + id: string; + name: string; + slug: string; + version: number; + updatedAt: string; + updatedBy: string | null; + /** Values are null until the user unlocks them with an authenticator code. */ + vars: { key: string; value: string | null }[]; + }; + access: "manage" | "edit" | "view"; + /** When the user's server-side unlock ends; null while values are locked. */ + revealUntil: string | null; + /** False when the user has no 2FA: values come unlocked. */ + revealRequired?: boolean; +}; + +function CliDialog({ open, onOpenChange, path }: { open: boolean; onOpenChange: (o: boolean) => void; path: string }) { + const origin = typeof window !== "undefined" ? window.location.origin : ""; + const dotenv = `curl -fsS -H "Authorization: Bearer $ENVOY_TOKEN" \\\n "${origin}/api/v1/${path}?format=dotenv" > .env`; + const json = `curl -fsS -H "Authorization: Bearer $ENVOY_TOKEN" \\\n "${origin}/api/v1/${path}"`; + return ( + + {/* Keep focus on the dialog itself; focusing the first copy button would pop its tooltip open. */} + e.preventDefault()}> + + Fetch from the command line + { + <> + Use a personal access token from{" "} + + API tokens + + . It gets the same access you have, so removing you from the team also cuts off the token. + + } + +
+ {[ + { label: "Write a .env file", code: dotenv }, + { label: "Get JSON", code: json }, + ].map((s) => ( +
+

{s.label}

+
+ {/* Long URLs wrap instead of scrolling sideways; the copy button sits in the corner. */} +
{s.code}
+
+ +
+
+
+ ))} +
+
+
+ ); +} + +function EnvNameDialog({ + open, + onOpenChange, + title, + initial, + submitLabel, + onSubmit, +}: { + open: boolean; + onOpenChange: (o: boolean) => void; + title: string; + initial: string; + submitLabel: string; + onSubmit: (name: string) => Promise; +}) { + return ( + onSubmit(name)} + /> + ); +} + +function ProjectView() { + const params = useParams<{ org: string; team: string; project: string }>(); + const search = useSearchParams(); + const router = useRouter(); + const { mutate: mutateOrg, isAdmin } = useOrg(); + const base = `/api/orgs/${params.org}/teams/${params.team}/projects/${params.project}`; + + const { data, error, mutate } = useSWR(base, fetcher); + const envs = data?.environments ?? []; + const requested = search.get("env"); + const current = envs.find((e) => e.slug === requested) ?? envs.find((e) => e.slug === "development") ?? envs[0]; + + const envKey = current ? `${base}/envs/${current.slug}` : null; + const { + data: envData, + error: envError, + mutate: mutateEnv, + } = useSWR(envKey, fetcher, { revalidateOnFocus: false, revalidateIfStale: false }); + + const reloadEnv = useCallback(() => mutateEnv(), [mutateEnv]); + + const [dirty, setDirty] = useState(false); + const [cliOpen, setCliOpen] = useState(false); + const [addEnvOpen, setAddEnvOpen] = useState(false); + const [renameEnvOpen, setRenameEnvOpen] = useState(false); + const [deleteEnvOpen, setDeleteEnvOpen] = useState(false); + const [deleteProjectOpen, setDeleteProjectOpen] = useState(false); + const [accessOpen, setAccessOpen] = useState(false); + const [moveOpen, setMoveOpen] = useState(false); + /** Environment the user tried to switch to while the current one has unsaved edits. */ + const [pendingEnv, setPendingEnv] = useState(null); + const onDirtyChange = useCallback((d: boolean) => setDirty(d), []); + + if (error) return mutate()} />; + if (!data) return ; + + const canEdit = data.access !== "view"; + const pagePath = `/o/${params.org}/t/${params.team}/p/${params.project}`; + + const goToEnv = (slug: string) => router.replace(`${pagePath}?env=${slug}`, { scroll: false }); + const switchEnv = (slug: string) => { + if (slug === current?.slug) return; + if (dirty) setPendingEnv(slug); // ask first, in an app dialog + else goToEnv(slug); + }; + const pendingEnvName = envs.find((e) => e.slug === pendingEnv)?.name; + + return ( + + + {data.project.name} + {data.project.restricted && } + {!canEdit && Read-only} + + } + description={data.project.description || undefined} + actions={ + + + {(canEdit || isAdmin) && ( + + + + + + {canEdit && ( + setAddEnvOpen(true)}> + Add environment + + )} + {canEdit && current && ( + setRenameEnvOpen(true)}> + Rename {current.name} + + )} + {isAdmin && ( + <> + setAccessOpen(true)}> + Manage access + + (dirty ? toast.error("Save or discard your changes first.") : setMoveOpen(true))} + > + Move to another team + + + {current && envs.length > 1 && ( + setDeleteEnvOpen(true)}> + Delete {current.name} + + )} + setDeleteProjectOpen(true)}> + Delete project + + + )} + + + )} + + } + /> + +
+
+ {/* Manual activation: focus alone (e.g. returning from the discard dialog) must not switch environments. */} + + + {envs.map((env) => ( + + + {env.name} + {env.varCount} + + ))} + + + {canEdit && ( + + + + + Add environment + + )} +
+ {envData && ( +

+ {envData.environment.updatedBy + ? `Saved by ${envData.environment.updatedBy} ${timeAgo(envData.environment.updatedAt)}` + : `Created ${timeAgo(envData.environment.updatedAt)}`} +

+ )} +
+ + {envError ? ( +

{(envError as Error).message}

+ ) : !envData || !current ? ( +
+ + {[0, 1, 2, 3, 4].map((i) => ( + + ))} +
+ ) : ( + { + mutateEnv(); + mutate(); + }} + /> + )} + + + {addEnvOpen && ( + { + const res = await api<{ environment: { slug: string } }>(`${base}/envs`, { body: { name } }); + await mutate(); + toast.success(`Added ${name}`); + router.replace(`${pagePath}?env=${res.environment.slug}`, { scroll: false }); + }} + /> + )} + {renameEnvOpen && current && ( + { + await api(`${base}/envs/${current.slug}`, { method: "PATCH", body: { name } }); + await Promise.all([mutate(), mutateEnv()]); + toast.success("Environment renamed"); + }} + /> + )} + { + toast.success(`Moved ${data.project.name} to ${teamName}`); + await mutateOrg(); + router.replace(`/o/${params.org}/t/${teamSlug}/p/${data.project.slug}${current ? `?env=${current.slug}` : ""}`); + }} + /> + {accessOpen && ( + { + mutate(); + mutateOrg(); + }} + /> + )} + !o && setPendingEnv(null)} + title="Discard unsaved changes?" + description={ + <> + Your edits to {current?.name} haven't been saved. Switching to{" "} + {pendingEnvName} discards them. + + } + cancelLabel="Keep editing" + actionLabel="Discard and switch" + onConfirm={() => { + if (pendingEnv) goToEnv(pendingEnv); + setPendingEnv(null); + }} + /> + { + try { + await api(`${base}/envs/${current!.slug}`, { method: "DELETE" }); + toast.success(`Deleted ${current!.name}`); + await mutate(); + router.replace(pagePath, { scroll: false }); + } catch (e: any) { + toast.error(e.message); + } + }} + /> + { + try { + await api(base, { method: "DELETE", body: { confirm: typed } }); + toast.success(`Deleted ${data.project.name}`); + await mutateOrg(); + router.push(`/o/${params.org}/t/${params.team}`); + } catch (e: any) { + toast.error(e.message); + } + }} + /> +
+ ); +} + +export default function ProjectPage() { + return ( + }> + + + ); +} diff --git a/web-app/src/app/(app)/o/[org]/t/[team]/page.tsx b/web-app/src/app/(app)/o/[org]/t/[team]/page.tsx new file mode 100644 index 0000000..85653ec --- /dev/null +++ b/web-app/src/app/(app)/o/[org]/t/[team]/page.tsx @@ -0,0 +1,495 @@ +"use client"; + +import { Suspense, useState } from "react"; +import Link from "next/link"; +import { useParams, useRouter, useSearchParams } from "next/navigation"; +import useSWR from "swr"; +import { toast } from "sonner"; +import { FolderPlus, MoreHorizontal, Pencil, Trash2, UserPlus, Users } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"; +import { PageContainer } from "@/components/app/app-shell"; +import { CreateProjectDialog } from "@/components/app/dialogs"; +import { QuickCreateDialog } from "@/components/quick-create-dialog"; +import { DialogCancel, DialogSubmit } from "@/components/dialog-actions"; +import { ROLE_LABEL, useOrg } from "@/components/app/org-context"; +import { PeoplePicker } from "@/components/app/people-picker"; +import { RestrictedLock } from "@/components/app/project-access-dialog"; +import { LoadError, LoadingPage } from "@/components/app/states"; +import { api, fetcher } from "@/lib/api-client"; +import { timeAgo } from "@/lib/format"; +import { FormField, UserAvatar, EmptyState, LoadMore, PageHeader, ToneBadge, ToneTile } from "@/components/common"; +import { ConfirmDialog } from "@/components/confirm-dialog"; +import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem } from "@/components/ui/dropdown-menu"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { roleTone } from "@/lib/tones"; +import { cn } from "@/lib/utils"; +import { usePaginated } from "@/lib/use-paginated"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { Skeleton } from "@/components/ui/skeleton"; + +type TeamData = { + team: { id: string; name: string; slug: string; description: string; access: "manage" | "edit" | "view"; myRole: string | null }; + orgRole: string; + projectCount: number; + memberCount: number; +}; + +type OrgMember = { userId: string; name: string; email: string; role: string }; + +function AddMemberDialog({ + open, + onOpenChange, + existing, + onAdded, +}: { + open: boolean; + onOpenChange: (o: boolean) => void; + existing: string[]; + onAdded: () => void; +}) { + const { slug } = useOrg(); + const { team } = useParams<{ team: string }>(); + const { data } = useSWR<{ members: OrgMember[] }>(open ? `/api/orgs/${slug}/members?limit=100` : null, fetcher); + const [userId, setUserId] = useState(""); + const [role, setRole] = useState<"editor" | "viewer">("viewer"); + const [busy, setBusy] = useState(false); + const [error, setError] = useState(null); + + const candidates = (data?.members ?? []).filter((m) => !existing.includes(String(m.userId))); + + return ( + + {/* Start in the people search, not on the "invite them" link in the description. */} + { + e.preventDefault(); + requestAnimationFrame(() => document.querySelector("[data-people-search]")?.focus()); + }} + > + + Add team member + { + <> + Choose someone already in the organization. To add someone new,{" "} + + invite them + {" "} + and pick this team. + + } + +
{ + e.preventDefault(); + setBusy(true); + setError(null); + try { + await api(`/api/orgs/${slug}/teams/${team}/members`, { body: { userId, role } }); + toast.success("Added to team"); + onAdded(); + onOpenChange(false); + setUserId(""); + } catch (err: any) { + setError(err.message); + } finally { + setBusy(false); + } + }} + > +
+

Person

+ {data ? ( + ({ id: String(m.userId), name: m.name, email: m.email }))} + isChecked={(id) => id === userId} + onPick={setUserId} + emptyText="Everyone in the organization is already on this team." + /> + ) : ( + + )} + {error &&

{error}

} +
+ + + + + onOpenChange(false)} /> + + Add to team + + +
+
+
+ ); +} + +function EditTeamDialog({ + open, + onOpenChange, + team, + onSaved, +}: { + open: boolean; + onOpenChange: (o: boolean) => void; + team: TeamData["team"]; + onSaved: () => void; +}) { + const { slug } = useOrg(); + return ( + The URL stays /t/{team.slug}} + submitLabel="Save changes" + onSubmit={async (name, description) => { + await api(`/api/orgs/${slug}/teams/${team.slug}`, { method: "PATCH", body: { name, description } }); + toast.success("Team updated"); + onSaved(); + }} + /> + ); +} + +type Project = { + id: string; + name: string; + slug: string; + description: string; + restricted?: boolean; + updatedAt: string; + envCount: number; + varCount: number; +}; +type Member = { userId: string; name: string; email: string; image: string | null; role: "editor" | "viewer" }; +type Tab = "projects" | "members"; + +function TabCount({ n }: { n: number | undefined }) { + return {n ?? "–"}; +} + +function ProjectCard({ p, href }: { p: Project; href: string }) { + return ( + +
+ +
+

+ {p.name} + {p.restricted && } +

+

{p.slug}

+
+
+

+ {p.description || "No description"} +

+
+ + {p.envCount} environment{p.envCount === 1 ? "" : "s"} + + + + {p.varCount} variable{p.varCount === 1 ? "" : "s"} + + {timeAgo(p.updatedAt)} +
+ + ); +} + +function TeamView() { + const params = useParams<{ org: string; team: string }>(); + const router = useRouter(); + const search = useSearchParams(); + const { slug, mutate: mutateOrg, isAdmin } = useOrg(); + const key = `/api/orgs/${params.org}/teams/${params.team}`; + const { data, error, mutate } = useSWR(key, fetcher); + const projects = usePaginated(data ? `${key}/projects` : null, "projects"); + const members = usePaginated(data ? `${key}/members` : null, "members"); + + const tab: Tab = search.get("tab") === "members" ? "members" : "projects"; + const setTab = (t: string) => router.replace(t === "members" ? `?tab=members` : "?", { scroll: false }); + + const [newProject, setNewProject] = useState(false); + const [addMember, setAddMember] = useState(false); + const [editTeam, setEditTeam] = useState(false); + const [deleteTeam, setDeleteTeam] = useState(false); + const [removing, setRemoving] = useState(null); + + if (error) return mutate()} />; + if (!data) return ; + + const { team, projectCount, memberCount } = data; + const canEdit = team.access !== "view"; + + const refreshMembers = () => { + members.mutate(); + mutate(); + }; + + async function changeRole(userId: string, role: string) { + try { + await api(`${key}/members/${userId}`, { method: "PATCH", body: { role } }); + toast.success("Access updated"); + members.mutate(); + } catch (e: any) { + toast.error(e.message); + } + } + + const tabAction = + tab === "projects" + ? canEdit && ( + + ) + : isAdmin && ( + + ); + + return ( + + + {team.name} + {team.access === "view" && Read-only} + {team.access === "edit" && Editor} + + } + description={team.description || undefined} + actions={ + isAdmin && ( + + + + + + setEditTeam(true)}> + Edit team + + setDeleteTeam(true)}> + Delete team + + + + ) + } + /> + + + {/* Tab row: tabs on the left sit on the divider; the open tab's action is on the right. */} +
+
+ + + Projects + + + Members + + +
+
{tabAction}
+
+ + + {projects.isLoading ? ( +
+ {[0, 1, 2].map((i) => ( + + ))} +
+ ) : projects.items.length === 0 ? ( + setNewProject(true)}> + New project + + ) + } + > + {canEdit + ? "A project holds the variables for one app or service, split into environments." + : "When editors create projects in this team, they'll appear here."} + + ) : ( + <> +
    + {projects.items.map((p) => ( +
  • + +
  • + ))} +
+ + + )} +
+ + + {members.isLoading ? ( + + ) : members.items.length === 0 ? ( +
+ + + +
+

No members yet

+

+ Add people from your organization as editors or{" "} + viewers. Owners and admins can already open every team. +

+
+
+ ) : ( + <> +
    + {members.items.map((m) => ( +
  • + +
    +

    {m.name}

    +

    {m.email}

    +
    + {isAdmin ? ( + <> + + + + ) : ( + {ROLE_LABEL[m.role]} + )} +
  • + ))} +
+ + + )} +
+
+ + { + projects.mutate(); + mutate(); + }} + /> + String(m.userId))} + onAdded={() => { + refreshMembers(); + setTab("members"); + }} + /> + {editTeam && ( + { + mutate(); + mutateOrg(); + }} + /> + )} + !o && setRemoving(null)} + title={`Remove ${removing?.name} from ${team.name}?`} + description="They'll lose access to this team's projects right away, including through their API tokens. They stay in the organization." + confirmText={removing?.email} + actionLabel="Remove from team" + onConfirm={async () => { + try { + await api(`${key}/members/${removing!.userId}`, { method: "DELETE" }); + toast.success("Removed from team"); + refreshMembers(); + } catch (e: any) { + toast.error(e.message); + } + }} + /> + { + try { + await api(key, { method: "DELETE", body: { confirm: typed || undefined } }); + toast.success(`Deleted ${team.name}`); + await mutateOrg(); + router.push(`/o/${slug}`); + } catch (e: any) { + toast.error(e.message); + } + }} + /> +
+ ); +} + +export default function TeamPage() { + // useSearchParams (the open tab) needs a Suspense boundary. + return ( + }> + + + ); +} diff --git a/web-app/src/app/(auth)/2fa/setup/page.tsx b/web-app/src/app/(auth)/2fa/setup/page.tsx new file mode 100644 index 0000000..e887399 --- /dev/null +++ b/web-app/src/app/(auth)/2fa/setup/page.tsx @@ -0,0 +1,284 @@ +"use client"; + +import { Suspense, useEffect, useState } from "react"; +import { Fingerprint, Smartphone } from "lucide-react"; +import { useSearchParams } from "next/navigation"; +import { signOut, useSession } from "next-auth/react"; +import { Button } from "@/components/ui/button"; +import { Checkbox } from "@/components/ui/checkbox"; +import { Label } from "@/components/ui/label"; +import { Skeleton } from "@/components/ui/skeleton"; +import { CopyButton, copyText } from "@/components/common"; +import { OtpField } from "@/components/otp-field"; +import { AuthHeading, FormError, safeCallback } from "@/components/auth/auth-bits"; +import { api } from "@/lib/api-client"; +import { createPasskey, passkeyError, passkeysSupported } from "@/lib/passkey-client"; +import { cn } from "@/lib/utils"; + +type Setup = { secret: string; uri: string; qr: string }; + +function downloadCodes(codes: string[], email?: string | null) { + const blob = new Blob( + [`Envoy backup codes${email ? ` for ${email}` : ""}\nEach code works once.\n\n${codes.join("\n")}\n`], + { type: "text/plain" } + ); + const a = document.createElement("a"); + a.href = URL.createObjectURL(blob); + a.download = "envoy-backup-codes.txt"; + a.click(); + URL.revokeObjectURL(a.href); +} + +const linkButton = "h-auto p-0 text-[13px] text-muted-foreground underline underline-offset-4 hover:text-foreground"; + +/** One way to do the second step, as a selectable card. */ +function MethodCard({ + icon: Icon, + title, + badge, + children, + onClick, + disabled, + busy, +}: { + icon: React.ElementType; + title: string; + badge?: string; + children: React.ReactNode; + onClick: () => void; + disabled?: boolean; + busy?: boolean; +}) { + return ( + + ); +} + +function SetupFlow() { + const params = useSearchParams(); + const callbackUrl = safeCallback(params.get("callbackUrl")); + const { data: session, update } = useSession(); + const [method, setMethod] = useState<"choose" | "totp">("choose"); + const [canPasskey, setCanPasskey] = useState(true); + const [creating, setCreating] = useState(false); + const [setup, setSetup] = useState(null); + const [showKey, setShowKey] = useState(false); + const [code, setCode] = useState(""); + const [error, setError] = useState(null); + const [busy, setBusy] = useState(false); + const [backupCodes, setBackupCodes] = useState(null); + const [saved, setSaved] = useState(false); + const email = session?.user?.email; + + useEffect(() => setCanPasskey(passkeysSupported()), []); + + // The authenticator secret is only created once someone picks that option. + useEffect(() => { + if (method !== "totp" || setup) return; + api("/api/2fa/setup", { method: "POST" }) + .then(setSetup) + .catch((e) => setError(e.message)); + }, [method, setup]); + + async function addPasskey() { + setCreating(true); + setError(null); + try { + const res = await createPasskey(); + if (res.mfaTicket) await update({ mfaTicket: res.mfaTicket }); + if (res.backupCodes) setBackupCodes(res.backupCodes); + else window.location.assign(callbackUrl); + } catch (e) { + const message = passkeyError(e); + if (message) setError(message); + } finally { + setCreating(false); + } + } + + async function enable(value: string) { + if (value.length !== 6) { + setError("Enter all 6 digits."); + return; + } + setBusy(true); + setError(null); + try { + const res = await api<{ backupCodes: string[] | null; mfaTicket: string }>("/api/2fa/enable", { body: { code: value } }); + await update({ mfaTicket: res.mfaTicket }); + if (res.backupCodes) setBackupCodes(res.backupCodes); + else window.location.assign(callbackUrl); + } catch (e: any) { + setError(e.message); + setCode(""); + } finally { + setBusy(false); + } + } + + if (backupCodes) { + return ( + <> + Each code signs you in once if your passkey or phone isn't at hand. +
    + {backupCodes.map((c) => ( +
  1. {c}
  2. + ))} +
+
+ + +
+
+ setSaved(v === true)} /> + +
+ + + ); + } + + if (method === "choose") { + return ( + <> + + Envoy holds secrets, and some organizations require two-factor. Pick how you'd like to confirm it's you. + + + + +
+ + {canPasskey ? ( + <> + Confirm with Touch ID, Face ID, Windows Hello or your password manager. + Nothing to type. + + ) : ( + "This browser doesn't support passkeys. Use an authenticator app, or switch browsers." + )} + + setMethod("totp")} disabled={creating}> + Type a 6-digit code from an app like 1Password or Google Authenticator. + +
+ +

You can add the other one later in Account settings.

+ +
+ + +
+ + ); + } + + return ( + <> + + Scan the code with your authenticator app, then enter the 6-digit code it shows. + + + + +
+ {setup ? ( + // eslint-disable-next-line @next/next/no-img-element + QR code for your authenticator app + ) : ( + + )} +
+ + {setup && + (showKey ? ( +
+ {setup.secret.match(/.{1,4}/g)?.join(" ")} + +
+ ) : ( + + ))} + +
{ + e.preventDefault(); + enable(code); + }} + className="mt-8 flex flex-col gap-4" + > + { + setCode(v); + if (error) setError(null); + }} + onComplete={enable} + disabled={busy || !setup} + invalid={!!error} + /> + + + +
+ + +
+ + ); +} + +export default function TwoFactorSetupPage() { + return ( + + + + ); +} diff --git a/web-app/src/app/(auth)/2fa/verify/page.tsx b/web-app/src/app/(auth)/2fa/verify/page.tsx new file mode 100644 index 0000000..a412ec9 --- /dev/null +++ b/web-app/src/app/(auth)/2fa/verify/page.tsx @@ -0,0 +1,166 @@ +"use client"; + +import { Suspense, useEffect, useState } from "react"; +import { Fingerprint } from "lucide-react"; +import { useSearchParams } from "next/navigation"; +import { signOut, useSession } from "next-auth/react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { AuthHeading, FormError, safeCallback } from "@/components/auth/auth-bits"; +import { OtpField } from "@/components/otp-field"; +import { api } from "@/lib/api-client"; +import { confirmWithPasskey, passkeyError, passkeysSupported } from "@/lib/passkey-client"; + +type Mode = "passkey" | "totp" | "backup"; +type Methods = { totp: boolean; passkeys: number; backupCodes: number }; + +const linkButton = "h-auto p-0 text-[13px] text-muted-foreground underline underline-offset-4 hover:text-foreground"; + +function VerifyFlow() { + const params = useSearchParams(); + const callbackUrl = safeCallback(params.get("callbackUrl")); + const { update } = useSession(); + const [methods, setMethods] = useState(null); + const [mode, setMode] = useState("totp"); + const [code, setCode] = useState(""); + const [backup, setBackup] = useState(""); + const [error, setError] = useState(null); + const [busy, setBusy] = useState(false); + + // Start with a passkey when the account has one and this browser can use it. + useEffect(() => { + api("/api/2fa/methods") + .then((m) => { + setMethods(m); + setMode(m.passkeys > 0 && passkeysSupported() ? "passkey" : m.totp ? "totp" : "backup"); + }) + .catch(() => setMethods({ totp: true, passkeys: 0, backupCodes: 0 })); + }, []); + + async function finish(mfaTicket: string, backupCodesRemaining?: number) { + const next = await update({ mfaTicket }); + if (!next?.user?.mfaVerified) throw new Error("Verification expired. Try again."); + if (typeof backupCodesRemaining === "number" && backupCodesRemaining <= 3) { + sessionStorage.setItem("envoy:lowBackupCodes", String(backupCodesRemaining)); + } + window.location.assign(callbackUrl); + } + + async function withPasskey() { + setBusy(true); + setError(null); + try { + const res = await confirmWithPasskey<{ mfaTicket: string }>("login"); + await finish(res.mfaTicket); + } catch (e) { + const message = passkeyError(e); + if (message) setError(message); + setBusy(false); + } + } + + async function verify(body: { code: string } | { backupCode: string }) { + if ("code" in body && body.code.length !== 6) { + setError("Enter all 6 digits."); + return; + } + if ("backupCode" in body && body.backupCode.trim().length < 10) { + setError("Enter a full backup code."); + return; + } + setBusy(true); + setError(null); + try { + const res = await api<{ mfaTicket: string; backupCodesRemaining?: number }>("/api/2fa/verify", { body }); + await finish(res.mfaTicket, res.backupCodesRemaining); + } catch (e: any) { + setError(e.message); + setCode(""); + setBusy(false); + } + } + + const switchTo = (m: Mode) => { + setMode(m); + setError(null); + }; + const others: { mode: Mode; label: string }[] = [ + ...(mode !== "passkey" && (methods?.passkeys ?? 0) > 0 && passkeysSupported() ? [{ mode: "passkey" as Mode, label: "Use a passkey" }] : []), + ...(mode !== "totp" && methods?.totp ? [{ mode: "totp" as Mode, label: "Use authenticator app" }] : []), + ...(mode !== "backup" ? [{ mode: "backup" as Mode, label: "Use a backup code" }] : []), + ]; + + return ( + <> + + {mode === "passkey" + ? "Use your passkey to finish signing in." + : mode === "totp" + ? "Enter the 6-digit code from your authenticator app." + : "Enter one of your backup codes."} + + + + + {mode === "passkey" ? ( + + ) : ( +
{ + e.preventDefault(); + verify(mode === "totp" ? { code } : { backupCode: backup }); + }} + className="flex flex-col gap-4" + > + {mode === "totp" ? ( + { + setCode(v); + if (error) setError(null); + }} + onComplete={(v) => verify({ code: v })} + disabled={busy || !methods} + invalid={!!error} + /> + ) : ( + setBackup(e.target.value)} + /> + )} + + + )} + +
+ {others.map((o) => ( + + ))} + +
+ + ); +} + +export default function TwoFactorVerifyPage() { + return ( + + + + ); +} diff --git a/web-app/src/app/(auth)/invite/[token]/page.tsx b/web-app/src/app/(auth)/invite/[token]/page.tsx new file mode 100644 index 0000000..9a57395 --- /dev/null +++ b/web-app/src/app/(auth)/invite/[token]/page.tsx @@ -0,0 +1,107 @@ +"use client"; + +import { useState } from "react"; +import Link from "next/link"; +import { useParams } from "next/navigation"; +import useSWR from "swr"; +import { useSession } from "next-auth/react"; +import { Button } from "@/components/ui/button"; +import { AuthHeading, FormError } from "@/components/auth/auth-bits"; +import { api, fetcher } from "@/lib/api-client"; +import { Skeleton } from "@/components/ui/skeleton"; + +type Preview = { + invite: { + org: string; + team: string | null; + orgRole: string; + teamRole: string | null; + invitedBy: string | null; + expiresAt: string; + status: "active" | "revoked" | "expired" | "used_up"; + }; +}; + +const STATUS_COPY: Record = { + revoked: "This invite was revoked. Ask an admin for a new one.", + expired: "This invite has expired. Ask an admin for a new one.", + used_up: "This invite has reached its usage limit. Ask an admin for a new one.", +}; + +export default function InvitePage() { + const { token } = useParams<{ token: string }>(); + const { data: session, status } = useSession(); + const { data, error: loadError } = useSWR(`/api/invites/preview/${token}`, fetcher); + const [error, setError] = useState(null); + const [busy, setBusy] = useState(false); + const here = `/invite/${token}`; + + if (loadError) { + return ( + + This invite link or code isn't valid. Check that you copied all of it, or ask an admin for a new invite. + + ); + } + if (!data) { + return ( +
+ + +
+ ); + } + + const { invite } = data; + const where = invite.team ? `the ${invite.team} team in ${invite.org}` : invite.org; + const role = invite.team ? `${invite.teamRole === "editor" ? "edit" : "read-only"} access` : `${invite.orgRole} access`; + + async function accept() { + setBusy(true); + setError(null); + try { + const res = await api<{ org: { slug: string }; team: { slug: string } | null }>("/api/invites/accept", { body: { token } }); + window.location.assign(res.team ? `/o/${res.org.slug}/t/${res.team.slug}` : `/o/${res.org.slug}`); + } catch (e: any) { + setError(e.message); + setBusy(false); + } + } + + const verified = session?.user?.mfaVerified; + + return ( + <> + + {invite.invitedBy ?? "An admin"} invited you to {where} with {role}. + + + + + {invite.status === "active" && + (status === "loading" ? ( + + ) : verified ? ( +
+ +

Signed in as {session?.user?.email}.

+
+ ) : session ? ( + + ) : ( +
+ + +
+ ))} + + ); +} diff --git a/web-app/src/app/(auth)/layout.tsx b/web-app/src/app/(auth)/layout.tsx new file mode 100644 index 0000000..dd66bf1 --- /dev/null +++ b/web-app/src/app/(auth)/layout.tsx @@ -0,0 +1,28 @@ +import Link from "next/link"; +import { Logo } from "@/components/brand/logo"; +import { ThemeToggle } from "@/components/theme-toggle"; +import { AuthAside } from "@/components/auth/auth-aside"; + +/** + * Every auth and onboarding screen: a brand panel on the left (lg and up) and the actual step on the right. + * Below lg the panel is hidden and the step takes the full width. + */ +export default function AuthLayout({ children }: { children: React.ReactNode }) { + return ( +
+ +
+
+ + + + +
+
+ {/* Pages opt into a wider column with data-wide (e.g. 2FA setup). */} +
{children}
+
+
+
+ ); +} diff --git a/web-app/src/app/(auth)/login/page.tsx b/web-app/src/app/(auth)/login/page.tsx new file mode 100644 index 0000000..0d646ca --- /dev/null +++ b/web-app/src/app/(auth)/login/page.tsx @@ -0,0 +1,81 @@ +"use client"; + +import { Suspense, useState } from "react"; +import Link from "next/link"; +import { useSearchParams } from "next/navigation"; +import { signIn } from "next-auth/react"; +import { Button } from "@/components/ui/button"; +import { AuthHeading, FormError, GoogleButton, OrDivider, authErrorMessage, safeCallback } from "@/components/auth/auth-bits"; +import { FormField } from "@/components/common"; +import { Input } from "@/components/ui/input"; + +function LoginForm() { + const params = useSearchParams(); + const callbackUrl = safeCallback(params.get("callbackUrl")); + const [error, setError] = useState(authErrorMessage(params.get("error"))); + const [busy, setBusy] = useState(false); + + async function onSubmit(e: React.FormEvent) { + e.preventDefault(); + const form = new FormData(e.currentTarget); + setBusy(true); + setError(null); + const res = await signIn("credentials", { + username: String(form.get("username") ?? ""), + password: String(form.get("password") ?? ""), + redirect: false, + }); + if (!res || res.error) { + setError(authErrorMessage(res?.error ?? "CredentialsSignin")); + setBusy(false); + return; + } + // Middleware routes the session to 2FA setup or verification before the destination. + window.location.assign(callbackUrl); + } + + return ( + <> + If you've set up a passkey or authenticator app, you'll confirm with it next. + + + + + + +
+ + + + + + + +
+

+ Signed up with Google? Set a password in Account settings to sign in with + email too. +

+ +

+ New to Envoy?{" "} + + Create an account + +

+ + ); +} + +export default function LoginPage() { + return ( + + + + ); +} diff --git a/web-app/src/app/(auth)/onboarding/page.tsx b/web-app/src/app/(auth)/onboarding/page.tsx new file mode 100644 index 0000000..83e1ef7 --- /dev/null +++ b/web-app/src/app/(auth)/onboarding/page.tsx @@ -0,0 +1,123 @@ +"use client"; + +import { useEffect, useRef, useState } from "react"; +import Link from "next/link"; +import useSWR from "swr"; +import { signOut, useSession } from "next-auth/react"; +import { Button } from "@/components/ui/button"; +import { AuthHeading, FormError } from "@/components/auth/auth-bits"; +import { api, fetcher } from "@/lib/api-client"; +import { slugify } from "@/lib/slug"; +import { FormField } from "@/components/common"; +import { Input } from "@/components/ui/input"; +import { InviteCodeField } from "@/components/invite-code-field"; +import { INVITE_CODE_LENGTH } from "@/lib/invite-code"; + +export default function OnboardingPage() { + const [name, setName] = useState(""); + const { data: session } = useSession(); + const prefilled = useRef(false); + // People who already belong to an organization come here to create or join another one: they get a way back. + const { data: orgs } = useSWR<{ orgs: unknown[] }>("/api/orgs?limit=1", fetcher); + const hasOrg = (orgs?.orgs.length ?? 0) > 0; + + // Suggest "'s Organization" once the session loads; never overwrite what the user typed. + useEffect(() => { + const first = session?.user?.name?.trim().split(/\s+/)[0]; + if (prefilled.current || !first) return; + prefilled.current = true; + setName((current) => current || `${first}'s Organization`); + }, [session]); + const [code, setCode] = useState(""); + const [error, setError] = useState(null); + const [busy, setBusy] = useState<"create" | "join" | null>(null); + + async function createOrg(e: React.FormEvent) { + e.preventDefault(); + setBusy("create"); + setError(null); + try { + const res = await api<{ org: { slug: string } }>("/api/orgs", { body: { name } }); + window.location.assign(`/o/${res.org.slug}`); + } catch (err: any) { + setError(err.message); + setBusy(null); + } + } + + async function join(e?: React.FormEvent) { + e?.preventDefault(); + if (code.length !== INVITE_CODE_LENGTH) return; + setBusy("join"); + setError(null); + try { + const res = await api<{ org: { slug: string }; team: { slug: string } | null }>("/api/invites/accept", { body: { token: code } }); + window.location.assign(res.team ? `/o/${res.org.slug}/t/${res.team.slug}` : `/o/${res.org.slug}`); + } catch (err: any) { + setError(err.message); + setBusy(null); + } + } + + return ( + <> + + {hasOrg ? ( + <>Create another organization, or join one with an invite code. + ) : ( + <> + Organizations hold your teams, projects and secrets. Create one for your company, or join one you've + been invited to. + + )} + + + + +
+

Create an organization

+
+ URL: /o/{slugify(name)} : "Usually your company or group name."} + > + setName(e.target.value)} required maxLength={64} placeholder="Acme Inc." /> + +
+ + {hasOrg && ( + + )} +
+
+
+ +
+

Join with an invite code

+

+ Enter the 10-character code an admin shared with you. Pasting the + whole invite link works too. +

+
+ + + +
+ + {!hasOrg && ( +

+ +

+ )} + + ); +} diff --git a/web-app/src/app/(auth)/signup/page.tsx b/web-app/src/app/(auth)/signup/page.tsx new file mode 100644 index 0000000..87347b8 --- /dev/null +++ b/web-app/src/app/(auth)/signup/page.tsx @@ -0,0 +1,41 @@ +"use client"; + +import { Suspense } from "react"; +import Link from "next/link"; +import { useSearchParams } from "next/navigation"; +import { AuthHeading, GoogleButton, safeCallback } from "@/components/auth/auth-bits"; + +/** + * New accounts are created with Google only, so every address is verified. People can set a password in + * Account settings afterwards and then sign in with email and password too. + */ +function SignupForm() { + const params = useSearchParams(); + const callbackUrl = safeCallback(params.get("callbackUrl")); + + return ( + <> + + Envoy accounts are created with Google, so we know the email address is + yours. You can set a password later in Account settings. + + + + +

+ Already have an account?{" "} + + Sign in + +

+ + ); +} + +export default function SignupPage() { + return ( + + + + ); +} diff --git a/web-app/src/app/api/(auth)/auth/[...nextauth]/options.ts b/web-app/src/app/api/(auth)/auth/[...nextauth]/options.ts index 2c77cda..a46355c 100644 --- a/web-app/src/app/api/(auth)/auth/[...nextauth]/options.ts +++ b/web-app/src/app/api/(auth)/auth/[...nextauth]/options.ts @@ -1,58 +1,56 @@ import { NextAuthOptions } from "next-auth"; -import CredentialsProvider from "next-auth/providers/credentials" +import CredentialsProvider from "next-auth/providers/credentials"; import Google from "next-auth/providers/google"; import connectMongo from "@/utils/connectMongo"; import User from "@/models/user"; +import { isMfaEnrolled } from "@/lib/passkeys"; import { comparePassword } from "@/utils/bcrypt"; import { sanitizeFilter } from "mongoose"; -import { loginLimiter } from "@/utils/limiter"; -import { getUserIP } from "@/utils/limiter"; - +import { loginLimiter, getUserIP } from "@/utils/limiter"; +import { sha256 } from "@/lib/totp"; + +/** + * Session lifecycle: + * 1. Password or Google sign-in → JWT with mfaVerified=false. + * 2. Middleware sends the user to /2fa/setup (no TOTP yet) or /2fa/verify. + * 3. /api/2fa/* verifies the code and stores a one-time ticket on the user. + * 4. The client calls session.update({ mfaTicket }); the jwt callback consumes the + * ticket and flips mfaVerified=true. Codes are never trusted from the client. + * Bumping user.sessionVersion revokes every outstanding JWT (checked on each refresh). + */ export const authOptions: NextAuthOptions = { providers: [ CredentialsProvider({ name: "Credentials", credentials: { - username: { label: "Username", type: "text" }, - password: { label: "Password", type: "password" } + username: { label: "Email or username", type: "text" }, + password: { label: "Password", type: "password" }, }, async authorize(credentials, req: any) { - if (!credentials) { - throw new Error("No credentials provided"); + if (!credentials?.username || !credentials?.password) { + throw new Error("Enter your email/username and password."); } const ip = getUserIP(req); - await loginLimiter.consume(ip); - try { + await loginLimiter.consume(ip); await connectMongo(); - const user = await User.findOne(sanitizeFilter({ - $or: [ - { email: credentials.username }, - { username: credentials.username } - ] - })).select("+password"); + const identifier = String(credentials.username).trim().toLowerCase(); + const user = await User.findOne( + sanitizeFilter({ $or: [{ email: identifier }, { username: identifier }] }) + ).select("+password"); - if (!user) { - await loginLimiter.consume(ip); + if (!user || !user.password) { + if (user && user.provider === "google") { + throw new Error("This account signs in with Google. Continue with Google, or set a password in Account settings first."); + } return null; } - if (user.provider !== "credentials") { - throw new Error("User does not use credentials for authentication. Try signing in with Google."); - } - - const isPasswordValid = await comparePassword( - credentials.password, - user.password - ); - - if (!isPasswordValid) { - await loginLimiter.consume(ip); - return null; - } + const isPasswordValid = await comparePassword(credentials.password, user.password); + if (!isPasswordValid) return null; await loginLimiter.delete(ip); @@ -62,27 +60,24 @@ export const authOptions: NextAuthOptions = { email: user.email, }; } catch (error: any) { - if (error?.msBeforeNext) { const retrySecs = Math.ceil(error.msBeforeNext / 1000); throw new Error(`Too many login attempts. Try again in ${retrySecs}s.`); } - throw new Error((error as Error).message || "Authentication failed"); } - } - }), Google({ + }, + }), + Google({ clientId: process.env.GOOGLE_CLIENT_ID!, clientSecret: process.env.GOOGLE_CLIENT_SECRET!, - profile: (profile) => { - return { - id: profile.sub || profile.id || profile.email, - name: profile.name || profile.email, - email: profile.email, - }; - }, + profile: (profile) => ({ + id: profile.sub || profile.id || profile.email, + name: profile.name || profile.email, + email: profile.email, + image: profile.picture, + }), }), - ], session: { @@ -97,37 +92,103 @@ export const authOptions: NextAuthOptions = { secret: process.env.NEXTAUTH_SECRET, + events: { + // Signing out also ends any "reveal values" unlock (see lib/reveal). + async signOut({ token }) { + if (!token?.id) return; + await connectMongo(); + await User.updateOne({ _id: token.id }, { $unset: { revealUntil: 1 } }); + }, + }, + callbacks: { - async jwt({ token, user, account, profile }) { + async signIn({ account, profile }) { + // Only accept Google accounts whose email Google has verified. + if (account?.provider === "google") { + return (profile as any)?.email_verified !== false; + } + return true; + }, + + async jwt({ token, user, account, profile, trigger, session }) { await connectMongo(); + // ── Fresh sign-in ────────────────────────────────────────────── if (account?.provider === "google") { - const email = profile?.email || token.email; - const name = profile?.name || token.name; - - const existingUser = await User.findOne({ email }); - - if (!existingUser) { - const newUser = await User.create({ - name, - email, - provider: "google", - }); - - token.id = newUser._id.toString(); - token.email = newUser.email; - token.name = newUser.name; - } else { - token.id = existingUser._id.toString(); - token.email = existingUser.email; - token.name = existingUser.name; + const email = String(profile?.email || token.email).toLowerCase(); + const name = profile?.name || token.name || email; + + const picture = (profile as any)?.picture as string | undefined; + + let dbUser = await User.findOne({ email }); + if (!dbUser) { + dbUser = await User.create({ name, email, provider: "google", emailVerified: true, image: picture }); + } else if (!dbUser.emailVerified || (picture && dbUser.image !== picture)) { + // Keep the Google profile picture current; it changes when the user updates it in Google. + dbUser.emailVerified = true; + if (picture) dbUser.image = picture; + await dbUser.save(); } + + return { + ...token, + id: dbUser._id.toString(), + email: dbUser.email, + name: dbUser.name, + picture: dbUser.image ?? null, + sv: dbUser.sessionVersion ?? 0, + totpEnabled: !!dbUser.totpEnabled, + mfaEnrolled: isMfaEnrolled(dbUser), + mfaVerified: false, + revoked: false, + }; } if (user && account?.provider === "credentials") { - token.id = user.id; - token.email = user.email; - token.name = user.name; + const dbUser = await User.findById(user.id).select("sessionVersion totpEnabled passkeyCount image"); + return { + ...token, + id: user.id, + email: user.email, + name: user.name, + picture: dbUser?.image ?? null, + sv: dbUser?.sessionVersion ?? 0, + totpEnabled: !!dbUser?.totpEnabled, + mfaEnrolled: isMfaEnrolled(dbUser), + mfaVerified: false, + revoked: false, + }; + } + + if (!token.id || token.revoked) return token; + + // ── Refresh / update: re-check revocation and consume MFA tickets ── + const dbUser = await User.findById(token.id).select( + "name email image sessionVersion totpEnabled passkeyCount +mfaTicketHash +mfaTicketExpiry" + ); + + if (!dbUser || (dbUser.sessionVersion ?? 0) !== (token.sv ?? 0)) { + return { ...token, revoked: true, mfaVerified: false }; + } + + token.name = dbUser.name; + token.picture = dbUser.image ?? null; + token.totpEnabled = !!dbUser.totpEnabled; + token.mfaEnrolled = isMfaEnrolled(dbUser); + if (!token.mfaEnrolled) token.mfaVerified = false; + + const ticket = trigger === "update" ? (session as any)?.mfaTicket : undefined; + if (typeof ticket === "string" && dbUser.mfaTicketHash) { + const valid = + sha256(ticket) === dbUser.mfaTicketHash && + dbUser.mfaTicketExpiry && + dbUser.mfaTicketExpiry.getTime() > Date.now(); + + await User.updateOne({ _id: dbUser._id }, { $unset: { mfaTicketHash: 1, mfaTicketExpiry: 1 } }); + + if (valid && token.mfaEnrolled) { + token.mfaVerified = true; + } } return token; @@ -135,14 +196,16 @@ export const authOptions: NextAuthOptions = { async session({ session, token }) { if (token?.id) { - session.user.id = token.id.toString(); + session.user.id = String(token.id); session.user.email = token.email ?? session.user.email; session.user.name = token.name ?? session.user.name; + session.user.image = (token.picture as string | null | undefined) ?? null; + session.user.mfaVerified = !!token.mfaVerified; + session.user.totpEnabled = !!token.totpEnabled; + session.user.mfaEnrolled = !!token.mfaEnrolled; + session.user.revoked = !!token.revoked; } - return session; }, - } - - -} \ No newline at end of file + }, +}; diff --git a/web-app/src/app/api/(user)/env/get/route.ts b/web-app/src/app/api/(user)/env/get/route.ts deleted file mode 100644 index 44e2f02..0000000 --- a/web-app/src/app/api/(user)/env/get/route.ts +++ /dev/null @@ -1,39 +0,0 @@ -import connectMongo from "@/utils/connectMongo"; -import Group from "@/models/group"; -import BasicGroup from "@/models/basicGroup"; -import { NextRequest, NextResponse } from "next/server"; -import { generalLimiter } from "@/utils/limiter"; -import { getUserIP } from "@/utils/limiter"; - -export async function GET(request: NextRequest) { - - const token = request.headers.get("Authorization")?.split(" ")[1]; - const userIP = await getUserIP(request); - - if (!token) { - return NextResponse.json({ message: "Group Token not provided." }, { status: 401 }); - } - - try { - await generalLimiter.consume(userIP); - } catch (error) { - return NextResponse.json({ message: "Too many requests. Please try again later." }, { status: 429 }); - } - - try { - - await connectMongo(); - - const group = await Group.findOne({ token }) || await BasicGroup.findOne({ token }); - - if (!group) { - return NextResponse.json({ message: "Group not found." }, { status: 404 }); - } - - return NextResponse.json({ group }) - - } catch (error: any) { - return NextResponse.json({ message: "Internal Server Error." }, { status: 500 }); - } - -} \ No newline at end of file diff --git a/web-app/src/app/api/(user)/env/route.ts b/web-app/src/app/api/(user)/env/route.ts deleted file mode 100644 index 88fee06..0000000 --- a/web-app/src/app/api/(user)/env/route.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { NextResponse, NextRequest } from "next/server"; -import connectMongo from "@/utils/connectMongo"; -import Group from "@/models/group"; -import { getToken } from "next-auth/jwt"; -import { encrypt } from "@/utils/encryption"; -import { generalLimiter } from "@/utils/limiter"; -import { getUserIP } from "@/utils/limiter"; - -export async function POST(request: NextRequest) { - const token = await getToken({ req: request, secret: process.env.NEXTAUTH_SECRET }); - const userId = token?.id; - const ip = await getUserIP(request); - - - if (!userId) { - return NextResponse.json({ message: "Unauthorized" }, { status: 401 }); - } - - try { - await generalLimiter.consume(ip); - } catch (error) { - return NextResponse.json({ message: "Too many requests. Please try again later." }, { status: 429 }); - } - - const { groupToken, envs } = await request.json(); - - if (!groupToken || !envs) { - return NextResponse.json({ message: "Group token and environment variables are required." }, { status: 400 }); - } - - if (!Array.isArray(envs) || !envs.every(e => e.key && e.value)) { - return NextResponse.json({ message: "Invalid envs format." }, { status: 400 }); - } - - try { - await connectMongo(); - - const group = await Group.findOne({ token: groupToken, owner: userId }); - - if (!group) { - return NextResponse.json({ message: "Group not found or you do not have permission to access it." }, { status: 404 }); - } - - group.envs = envs.map(({ key, value }) => ({ - key, - value: encrypt(value) - })); - await group.save(); - - return NextResponse.json({ message: "Environment variables updated successfully.", envs: group.envs }, { status: 200 }); - - } catch (error: any) { - return NextResponse.json({ message: "Internal Server Error." }, { status: 500 }); - } -} \ No newline at end of file diff --git a/web-app/src/app/api/(user)/group/create/route.ts b/web-app/src/app/api/(user)/group/create/route.ts deleted file mode 100644 index ff308b7..0000000 --- a/web-app/src/app/api/(user)/group/create/route.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; -import connectMongo from "@/utils/connectMongo"; -import Group from "@/models/group"; -import { getToken } from "next-auth/jwt"; -import { randomUUID } from "crypto"; -import Workspace from "@/models/workspace"; -import User from "@/models/user"; -import { generalLimiter, getUserIP } from "@/utils/limiter"; - -export async function POST(request: NextRequest) { - - const token = await getToken({ req: request, secret: process.env.NEXTAUTH_SECRET }); - const userId = token?.id; - - if (!userId) { - return NextResponse.json({ message: "Unauthorized" }, { status: 401 }); - } - - try { - const ip = getUserIP(request); - await generalLimiter.consume(ip); - } catch (error) { - return NextResponse.json({ message: "Too many requests. Please try again later." }, { status: 429 }); - } - - const { name, workspaceId, isPublic } = await request.json(); - - if (!workspaceId || !name) { - return NextResponse.json({ message: "Workspace ID and name are required." }, { status: 400 }); - } - - - try { - await connectMongo(); - - const workspace = await Workspace.findOne({ _id: workspaceId, owner: userId }); - - if (!workspace) { - return NextResponse.json({ message: "Workspace not found." }, { status: 404 }); - } - - const user = await User.findById(userId); - - if (!user) { - return NextResponse.json({ message: "User not found." }, { status: 404 }); - } - - let uuid = randomUUID(); - - const existingToken = await Group.exists({ token: uuid }); - - if (existingToken) { - uuid = randomUUID(); - } - - const group = await Group.create({ - name, - workspace: workspaceId, - owner: userId, - token: uuid, - isPublic, - }); - - workspace.groups.push(group._id); - await workspace.save(); - - return NextResponse.json({ message: "Group created successfully.", group }, { status: 201 }); - - } catch (error: any) { - return NextResponse.json({ message: "Internal Server Error." }, { status: 500 }); - } - -} \ No newline at end of file diff --git a/web-app/src/app/api/(user)/group/createBasic/route.ts b/web-app/src/app/api/(user)/group/createBasic/route.ts deleted file mode 100644 index 31f31d2..0000000 --- a/web-app/src/app/api/(user)/group/createBasic/route.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { NextResponse, NextRequest } from "next/server"; -import connectMongo from "@/utils/connectMongo"; -import BasicGroup from "@/models/basicGroup"; -import { randomUUID } from "crypto"; -import { generalLimiter, getUserIP } from "@/utils/limiter"; - -export async function POST(request: NextRequest) { - - const { envs, expireAt } = await request.json(); - - if (!envs || !expireAt) { - return NextResponse.json({ message: "Envs and expireAt are required." }, { status: 400 }); - } - - if (!Array.isArray(envs) || envs.length === 0) { - return NextResponse.json({ message: "Envs must be a non-empty array." }, { status: 400 }); - } - - if (typeof expireAt !== 'number' || expireAt <= 0) { - return NextResponse.json({ message: "ExpireAt must be a positive number." }, { status: 400 }); - } - - try { - const ip = getUserIP(request); - await generalLimiter.consume(ip); - } catch (error) { - return NextResponse.json({ message: "Too many requests. Please try again later." }, { status: 429 }); - } - - try { - - await connectMongo(); - - let uuid = randomUUID(); - - if (await BasicGroup.exists({ token: uuid })) { - uuid = randomUUID(); - } - - const basicGroup = await BasicGroup.create({ - token: uuid, - envs, - expireAt: new Date(Date.now() + expireAt * 1000), // expireAt in seconds - }); - - return NextResponse.json({ message: "Group Created Successfully.", token: basicGroup.token }, { status: 201 }); - - } catch (error: any) { - return NextResponse.json({ message: "Internal Server Error." }, { status: 500 }); - } - -} \ No newline at end of file diff --git a/web-app/src/app/api/(user)/group/delete/route.ts b/web-app/src/app/api/(user)/group/delete/route.ts deleted file mode 100644 index 61e1afe..0000000 --- a/web-app/src/app/api/(user)/group/delete/route.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { NextResponse, NextRequest } from "next/server"; -import connectMongo from "@/utils/connectMongo"; -import Group from "@/models/group"; -import Workspace from "@/models/workspace"; -import { getToken } from "next-auth/jwt"; -import User from "@/models/user"; -import { generalLimiter, getUserIP } from "@/utils/limiter"; - -export async function DELETE(request: NextRequest) { - - const token = await getToken({ req: request, secret: process.env.NEXTAUTH_SECRET }); - const userId = token?.id; - - if (!userId) { - return NextResponse.json({ message: "Unauthorized" }, { status: 401 }); - } - - try { - const ip = getUserIP(request); - await generalLimiter.consume(ip); - } catch (error) { - return NextResponse.json({ message: "Too many requests. Please try again later." }, { status: 429 }); - } - - const { groupId } = await request.json(); - - if (!groupId) { - return NextResponse.json({ message: "Group ID is required." }, { status: 400 }); - } - - try { - - await connectMongo(); - - const group = await Group.findOneAndDelete({ _id: groupId, owner: userId }); - - if (!group) { - return NextResponse.json({ message: "Group not found or you do not have permission to delete it." }, { status: 404 }); - } - - const workspace = await Workspace.findById(group.workspace); - - if (!workspace) { - return NextResponse.json({ message: "Workspace not found." }, { status: 404 }); - } - - workspace.groups = workspace.groups.filter((id: any) => id.toString() !== groupId); - await workspace.save(); - - if (Array.isArray(group.members) && group.members.length > 0) { - await User.updateMany( - { _id: { $in: group.members } }, - { $pull: { otherGroups: group._id } } - ); - } - - - return NextResponse.json({ message: "Group deleted successfully." }, { status: 200 }); - - } catch (error: any) { - return NextResponse.json({ message: error.message }, { status: 500 }); - } - -} \ No newline at end of file diff --git a/web-app/src/app/api/(user)/group/member/add/route.ts b/web-app/src/app/api/(user)/group/member/add/route.ts deleted file mode 100644 index 6efd731..0000000 --- a/web-app/src/app/api/(user)/group/member/add/route.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { NextResponse, NextRequest } from "next/server"; -import connectMongo from "@/utils/connectMongo"; -import Group from "@/models/group"; -import { getToken } from "next-auth/jwt"; -import User from "@/models/user"; -import { generalLimiter, getUserIP } from "@/utils/limiter"; - -export async function POST(request: NextRequest) { - - const token = await getToken({ req: request, secret: process.env.NEXTAUTH_SECRET }); - const userId = token?.id; - - if (!userId) { - return NextResponse.json({ message: "Unauthorized" }, { status: 401 }); - } - - try { - const ip = getUserIP(request); - await generalLimiter.consume(ip); - } catch (error) { - return NextResponse.json({ message: "Too many requests. Please try again later." }, { status: 429 }); - } - - const { groupId, memberEmail } = await request.json(); - - if (!groupId || !memberEmail) { - return NextResponse.json({ message: "Group ID and member email are required." }, { status: 400 }); - } - - try { - await connectMongo(); - - const group = await Group.findOne({ _id: groupId, owner: userId }); - - if (!group) { - return NextResponse.json({ message: "Group not found or you do not have permission to access it." }, { status: 404 }); - } - - const member = await User.findOne({ email: memberEmail }); - - if (!member) { - return NextResponse.json({ message: "Member not found with provided email." }, { status: 404 }); - } - - if(member._id.toString() === userId) { - return NextResponse.json({ message: "You cannot add yourself as a member." }, { status: 400 }); - } - - if (group.members.map((member: any) => member.toString()).includes(member._id.toString())) { - return NextResponse.json({ message: "Member already exists in the group." }, { status: 400 }); - } - - group.members.push(member._id); - await group.save(); - - member.otherGroups.push(group._id); - await member.save(); - - return NextResponse.json({ message: "Member added successfully.", group }, { status: 200 }); - - } catch (error) { - return NextResponse.json({ message: "Internal Server Error" }, { status: 500 }); - } - -} \ No newline at end of file diff --git a/web-app/src/app/api/(user)/group/member/get/route.ts b/web-app/src/app/api/(user)/group/member/get/route.ts deleted file mode 100644 index ba4cd11..0000000 --- a/web-app/src/app/api/(user)/group/member/get/route.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; -import connectMongo from "@/utils/connectMongo"; -import Group from "@/models/group"; -import { getToken } from "next-auth/jwt"; -import { generalLimiter, getUserIP } from "@/utils/limiter"; - -export async function GET(request: NextRequest) { - - const token = await getToken({ req: request, secret: process.env.NEXTAUTH_SECRET }); - const userId = token?.id; - - if(!userId) { - return NextResponse.json({ message: "Unauthorized" }, { status: 401 }); - } - - try { - const ip = getUserIP(request); - await generalLimiter.consume(ip); - } catch (error) { - return NextResponse.json({ message: "Too many requests. Please try again later." }, { status: 429 }); - } - - const { searchParams } = new URL(request.url); - const groupToken = searchParams.get("token"); - - if (!groupToken) { - return NextResponse.json({ message: "Group token is required" }, { status: 400 }); - } - - try { - - await connectMongo(); - const group = await Group.findOne({ token: groupToken, owner: userId }).populate({ path: "members", select: "name email" }); - - if (!group) { - return NextResponse.json({ message: "Group not found or you do not have permission to view it" }, { status: 404 }); - } - - const members = group.members.filter((member: any) => member._id.toString() !== userId); - - return NextResponse.json({ group: group._id, members }, { status: 200 }); - - } catch (error: any) { - return NextResponse.json({ message: "Internal Server Error." }, { status: 500 }); - } - -} \ No newline at end of file diff --git a/web-app/src/app/api/(user)/group/member/remove/route.ts b/web-app/src/app/api/(user)/group/member/remove/route.ts deleted file mode 100644 index 93805e6..0000000 --- a/web-app/src/app/api/(user)/group/member/remove/route.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { NextResponse, NextRequest } from "next/server"; -import connectMongo from "@/utils/connectMongo"; -import Group from "@/models/group"; -import User from "@/models/user"; -import { getToken } from "next-auth/jwt"; -import { generalLimiter, getUserIP } from "@/utils/limiter"; - -export async function POST(request: NextRequest) { - - const token = await getToken({ req: request, secret: process.env.NEXTAUTH_SECRET }); - const userId = token?.id; - - if (!userId) { - return NextResponse.json({ message: "Unauthorized" }, { status: 401 }); - } - - try { - const ip = getUserIP(request); - await generalLimiter.consume(ip); - } catch (error) { - return NextResponse.json({ message: "Too many requests. Please try again later." }, { status: 429 }); - } - - const { groupId, memberEmail } = await request.json(); - - if (!groupId || !memberEmail) { - return NextResponse.json({ message: "Group ID and member email are required." }, { status: 400 }); - } - - try { - - await connectMongo(); - - const group = await Group.findOne({ _id: groupId, owner: userId }); - - if (!group) { - return NextResponse.json({ message: "Group not found or you do not have permission to access it." }, { status: 404 }); - } - - const member = await User.findOne({ email: memberEmail }); - - if (!member) { - return NextResponse.json({ message: "Member not found with provided email." }, { status: 404 }); - } - - if (group.members.map((member: any) => !member.toString()).includes(member._id.toString())) { - return NextResponse.json({ message: "Member does not exist in the group." }, { status: 400 }); - } - - if (member._id.toString() === group.owner.toString()) { - return NextResponse.json({ message: "You cannot remove the group owner from the group." }, { status: 400 }); - } - - group.members = group.members.filter((m: any) => m.toString() !== member._id.toString()); - await group.save(); - - member.otherGroups = member.otherGroups.filter((g: any) => g.toString() !== groupId); - await member.save(); - - return NextResponse.json({ message: "Member removed successfully." }, { status: 200 }); - - } catch (error: any) { - return NextResponse.json({ message: "Internal Server Error." }, { status: 500 }); - } - -} \ No newline at end of file diff --git a/web-app/src/app/api/(user)/group/route.ts b/web-app/src/app/api/(user)/group/route.ts deleted file mode 100644 index 9bee728..0000000 --- a/web-app/src/app/api/(user)/group/route.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; -import connectMongo from "@/utils/connectMongo"; -import Group from "@/models/group"; -import { getToken } from "next-auth/jwt"; -import BasicGroup from "@/models/basicGroup"; -import { generalLimiter, getUserIP } from "@/utils/limiter"; - -export async function GET(request: NextRequest) { - - const token = await getToken({ req: request, secret: process.env.NEXTAUTH_SECRET }); - const userId = token?.id; - - const { searchParams } = new URL(request.url); - const groupToken = searchParams.get("token"); - const type = searchParams.get("type"); - const edit = searchParams.get("edit") - - if (!groupToken) { - return NextResponse.json({ message: "Group token is required." }, { status: 400 }); - } - - try { - const userIP = getUserIP(request); - await generalLimiter.consume(userIP); - } catch (error) { - return NextResponse.json({ message: "Too many requests. Please try again later." }, { status: 429 }); - } - - if (type === "basic") { - try { - await connectMongo(); - const group = await BasicGroup.findOne({ token: groupToken }); - - if (!group) { - return NextResponse.json({ message: "Group not found." }, { status: 404 }); - } - - return NextResponse.json({ group }, { status: 200 }); - - } catch (error: any) { - return NextResponse.json({ message: "Internal Server Error." }, { status: 500 }); - } - } - - try { - await connectMongo(); - - const group = await Group.findOne({ token: groupToken }).populate([ - { path: "members", select: "name email" }, - { path: "owner", select: "name email" }, - ]); - - - if (!group) { - return NextResponse.json({ message: "Group not found." }, { status: 404 }); - } - - if (edit === "true") { - if (group.owner._id.toString() != userId) { - return NextResponse.json({ message: "Unauthorized for edits" }, { status: 401 }); - } - } - - if (group.isPublic) { - return NextResponse.json({ group }, { status: 200 }); - } - - if (!userId) { - return NextResponse.json({ message: "Unauthorized" }, { status: 401 }); - } - - if (group.owner._id.toString() === userId) { - return NextResponse.json({ group }, { status: 200 }); - } - - if (group.members.map((member: any) => member._id.toString()).includes(userId.toString())) { - return NextResponse.json({ group }, { status: 200 }); - } - - - return NextResponse.json({ message: "You do not have permission to access this group." }, { status: 403 }); - - } catch (error: any) { - return NextResponse.json({ message: "Internal Server Error." }, { status: 500 }); - } - -} \ No newline at end of file diff --git a/web-app/src/app/api/(user)/group/shared/route.ts b/web-app/src/app/api/(user)/group/shared/route.ts deleted file mode 100644 index f2abcda..0000000 --- a/web-app/src/app/api/(user)/group/shared/route.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; -import connectMongo from "@/utils/connectMongo"; -import User from "@/models/user"; -import Group from "@/models/group"; -import { getToken } from "next-auth/jwt"; -import { generalLimiter, getUserIP } from "@/utils/limiter"; - -export async function GET(request: NextRequest) { - const token = await getToken({ req: request, secret: process.env.NEXTAUTH_SECRET }); - const userId = token?.id; - - if (!userId) { - return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); - } - - try { - const ip = getUserIP(request); - await generalLimiter.consume(ip); - } catch (error) { - return NextResponse.json({ message: "Too many requests. Please try again later." }, { status: 429 }); - } - - try { - await connectMongo(); - const user = await User.findById(userId).populate('otherGroups'); - - return NextResponse.json({ message: "Shared groups fetched successfully.", otherGroups: user?.otherGroups }, { status: 200 }); - } catch (error: any) { - console.log("Error fetching shared groups:", error); - return NextResponse.json({ message: "Internal Server Error." }, { status: 500 }); - } -} \ No newline at end of file diff --git a/web-app/src/app/api/(user)/group/visibility/route.ts b/web-app/src/app/api/(user)/group/visibility/route.ts deleted file mode 100644 index f64093f..0000000 --- a/web-app/src/app/api/(user)/group/visibility/route.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; -import Group from "@/models/group"; -import connectMongo from "@/utils/connectMongo"; -import { getToken } from "next-auth/jwt"; -import { generalLimiter, getUserIP } from "@/utils/limiter"; - -export async function POST(request: NextRequest) { - const token = await getToken({ req: request, secret: process.env.NEXTAUTH_SECRET }); - const userId = token?.id; - - if (!userId) { - return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); - } - - try { - const userIP = getUserIP(request); - await generalLimiter.consume(userIP); - } catch (error) { - return NextResponse.json({ message: "Too many requests. Please try again later." }, { status: 429 }); - } - - const { groupToken } = await request.json(); - - if (!groupToken) { - return NextResponse.json({ error: "Group token is required." }, { status: 400 }); - } - - try { - await connectMongo(); - - const group = await Group.findOne({ token: groupToken, owner: userId }); - - if (!group) { - return NextResponse.json({ error: "Group not found or you do not have access." }, { status: 404 }); - } - - group.isPublic = !group.isPublic; - - await group.save(); - - return NextResponse.json({ message: "Group visibility updated.", isPublic: group.isPublic }, { status: 200 }); - } catch (error) { - console.error("Error updating group visibility:", error); - return NextResponse.json({ error: "Internal server error." }, { status: 500 }); - } -} \ No newline at end of file diff --git a/web-app/src/app/api/(user)/me/route.ts b/web-app/src/app/api/(user)/me/route.ts deleted file mode 100644 index 9dce49e..0000000 --- a/web-app/src/app/api/(user)/me/route.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { NextResponse, NextRequest } from "next/server"; -import connectMongo from "@/utils/connectMongo"; -import { generalLimiter, getUserIP } from "@/utils/limiter"; -import { getToken } from "next-auth/jwt"; -import User from "@/models/user"; - -export async function GET(request: NextRequest) { - const token = await getToken({ req: request, secret: process.env.NEXTAUTH_SECRET }); - const userId = token?.id; - - if (!userId) { - return NextResponse.json({ message: "Unauthorized" }, { status: 401 }); - } - - try { - const ip = getUserIP(request); - await generalLimiter.consume(ip); - } catch (error) { - return NextResponse.json({ message: "Too many requests. Please try again later." }, { status: 429 }); - } - - try { - await connectMongo(); - const user = await User.findById(userId).select("-password -__v"); - if (!user) { - return NextResponse.json({ message: "User not found." }, { status: 404 }); - } - - return NextResponse.json({ user }, { status: 200 }); - - } catch (error: any) { - console.log(error.message); - return NextResponse.json({ message: "Internal Server Error." }, { status: 500 }); - } -} \ No newline at end of file diff --git a/web-app/src/app/api/(user)/register/route.ts b/web-app/src/app/api/(user)/register/route.ts deleted file mode 100644 index 604d408..0000000 --- a/web-app/src/app/api/(user)/register/route.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { NextResponse, NextRequest } from "next/server"; -import User from "@/models/user"; -import connectMongo from "@/utils/connectMongo"; -import { sanitizeFilter } from "mongoose"; -import { hashPassword } from "@/utils/bcrypt"; -import { generalLimiter, getUserIP } from "@/utils/limiter"; - -export async function POST(request: NextRequest) { - - const { name, username, email, password } = await request.json(); - - if (!name || !username || !email || !password) { - return NextResponse.json({ message: "All fields are required" }, { status: 400 }); - } - - if(!email.match(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)) { - return NextResponse.json({ message: "Invalid email format." }, { status: 400 }); - } - - try { - const ip = getUserIP(request); - await generalLimiter.consume(ip); - } catch (error) { - return NextResponse.json({ message: "Too many requests. Please try again later." }, { status: 429 }); - } - - return NextResponse.json({ message: "Please use Google Provider for authentication." }, { status: 400 }); - - // try { - - // await connectMongo(); - - // const existingUser = await User.findOne(sanitizeFilter({ $or: [{ email }, { username }] })); - - // if (existingUser) { - // return NextResponse.json({ message: "User with this email or username already exists." }, { status: 400 }); - // } - - // const hashedPassword = await hashPassword(password); - - // const user = await User.create(sanitizeFilter({ name, email, username, password: hashedPassword })); - - // const userObj = user.toObject(); - // delete userObj.password; - - // return NextResponse.json({ message: "User registered successfully.", user: userObj }, { status: 201 }); - - // } catch (error) { - - // if (error instanceof Error) { - // return NextResponse.json({ message: "Internal Server Error." }, { status: 500 }); - // } - - // return NextResponse.json({ message: "An unknown error occurred." }, { status: 500 }); - // } - -} \ No newline at end of file diff --git a/web-app/src/app/api/(user)/workspace/create/route.ts b/web-app/src/app/api/(user)/workspace/create/route.ts deleted file mode 100644 index 8bdcffe..0000000 --- a/web-app/src/app/api/(user)/workspace/create/route.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { NextResponse, NextRequest } from "next/server"; -import connectMongo from "@/utils/connectMongo"; -import Workspace from "@/models/workspace"; -import { getToken } from "next-auth/jwt"; -import User from "@/models/user"; -import { generateShortUuid } from "custom-uuid"; -import { generalLimiter, getUserIP } from "@/utils/limiter"; - -export async function POST(request: NextRequest) { - - const token = await getToken({ req: request, secret: process.env.NEXTAUTH_SECRET }); - const userId = token?.id; - - if (!userId) { - return NextResponse.json({ message: "Unauthorized" }, { status: 401 }); - } - - try { - const ip = getUserIP(request); - await generalLimiter.consume(ip); - } catch (error) { - return NextResponse.json({ message: "Too many requests. Please try again later." }, { status: 429 }); - } - - const { name, description } = await request.json(); - - if (!name || !description) { - return NextResponse.json({ message: "Name and description are required." }, { status: 400 }); - } - - try { - - await connectMongo(); - const user = await User.findById(userId); - - if (!user) { - return NextResponse.json({ message: "User not found." }, { status: 404 }); - } - - let uuid = generateShortUuid(); - - if (await Workspace.exists({ uuid })) { - uuid = generateShortUuid(); - } - - const workspace = await Workspace.create({ name, description, owner: userId, uuid }); - - user.workspaces.push(workspace._id); - await user.save(); - - return NextResponse.json({ message: "Workspace created successfully.", workspace }, { status: 201 }); - - } catch (error: any) { - return NextResponse.json({ message: "Internal Server Error." }, { status: 500 }); - } - -} \ No newline at end of file diff --git a/web-app/src/app/api/(user)/workspace/delete/route.ts b/web-app/src/app/api/(user)/workspace/delete/route.ts deleted file mode 100644 index 6f5544e..0000000 --- a/web-app/src/app/api/(user)/workspace/delete/route.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { NextResponse, NextRequest } from "next/server"; -import mongoose from "mongoose"; -import connectMongo from "@/utils/connectMongo"; -import Workspace from "@/models/workspace"; -import Group from "@/models/group"; -import { getToken } from "next-auth/jwt"; -import User from "@/models/user"; -import { generalLimiter, getUserIP } from "@/utils/limiter"; - -export async function DELETE(request: NextRequest) { - const token = await getToken({ req: request, secret: process.env.NEXTAUTH_SECRET }); - const userId = token?.id; - - if (!userId) { - return NextResponse.json({ message: "Unauthorized" }, { status: 401 }); - } - - try { - const ip = getUserIP(request); - await generalLimiter.consume(ip); - } catch (error) { - return NextResponse.json({ message: "Too many requests. Please try again later." }, { status: 429 }); - } - - const { workspaceId } = await request.json(); - - if (!workspaceId) { - return NextResponse.json({ message: "Workspace ID is required." }, { status: 400 }); - } - - try { - await connectMongo(); - - const user = await User.findById(userId); - if (!user) { - return NextResponse.json({ message: "User not found." }, { status: 404 }); - } - - const workspace = await Workspace.findOneAndDelete({ _id: workspaceId, owner: userId }); - if (!workspace) { - return NextResponse.json({ message: "Workspace not found or you do not have permission to delete it." }, { status: 404 }); - } - - const groups = await Group.find({ workspace: workspaceId }); - const groupIds = groups.map(g => g._id); - - for (const groupId of groupIds) { - const result = await User.updateMany( - { otherGroups: groupId }, - { $pull: { otherGroups: groupId } } - ); - } - - await Group.deleteMany({ workspace: workspaceId }); - - user.workspaces = user.workspaces.filter((w: any) => w.toString() !== workspaceId); - await user.save(); - - return NextResponse.json({ message: "Workspace deleted successfully." }, { status: 200 }); - - } catch (error: any) { - console.error("Error deleting workspace:", error); - return NextResponse.json({ message: "Internal Server Error." }, { status: 500 }); - } -} diff --git a/web-app/src/app/api/(user)/workspace/edit/route.ts b/web-app/src/app/api/(user)/workspace/edit/route.ts deleted file mode 100644 index 418d3ca..0000000 --- a/web-app/src/app/api/(user)/workspace/edit/route.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { NextResponse, NextRequest } from "next/server"; -import connectMongo from "@/utils/connectMongo"; -import Workspace from "@/models/workspace"; -import { getToken } from "next-auth/jwt"; -import { generalLimiter, getUserIP } from "@/utils/limiter"; - -export async function PATCH(request: NextRequest) { - - const token = await getToken({ req: request, secret: process.env.NEXTAUTH_SECRET }); - const userId = token?.id; - - if (!userId) { - return NextResponse.json({ message: "Unauthorized" }, { status: 401 }); - } - - try { - const ip = getUserIP(request); - await generalLimiter.consume(ip); - } catch (error) { - return NextResponse.json({ message: "Too many requests. Please try again later." }, { status: 429 }); - } - - const { workspaceId, name, description } = await request.json(); - - try { - - await connectMongo(); - const workspace = await Workspace.findOneAndUpdate( - { _id: workspaceId, owner: userId }, - { name, description }, - { new: true } - ); - - if (!workspace) { - return NextResponse.json({ message: "Workspace not found or you do not have permission to edit it." }, { status: 404 }); - } - - return NextResponse.json({ message: "Workspace updated successfully.", workspace }, { status: 200 }); - - } catch (error: any) { - return NextResponse.json({ message: "Internal Server Error." }, { status: 500 }); - } - -} \ No newline at end of file diff --git a/web-app/src/app/api/(user)/workspace/route.ts b/web-app/src/app/api/(user)/workspace/route.ts deleted file mode 100644 index a4f1643..0000000 --- a/web-app/src/app/api/(user)/workspace/route.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; -import connectMongo from "@/utils/connectMongo"; -import Workspace from "@/models/workspace"; -import Group from "@/models/group"; -import { getToken } from "next-auth/jwt"; -import { generalLimiter, getUserIP } from "@/utils/limiter"; - -export async function GET(request: NextRequest) { - const token = await getToken({ req: request, secret: process.env.NEXTAUTH_SECRET }); - const userId = token?.id; - - if (!userId) { - return NextResponse.json({ message: "Unauthorized" }, { status: 401 }); - } - - try { - const ip = getUserIP(request); - await generalLimiter.consume(ip); - } catch (error) { - return NextResponse.json({ message: "Too many requests. Please try again later." }, { status: 429 }); - } - - const { searchParams } = new URL(request.url); - const workspaceUUID = searchParams.get("id"); - - await connectMongo(); - - if (workspaceUUID) { - try { - const workspace = await Workspace.findOne({ - uuid: workspaceUUID, - owner: userId, - }).populate({ - path: "groups", - model: Group, - select: "-envs", - }); - - if (!workspace) { - return NextResponse.json({ message: "Workspace not found." }, { status: 404 }); - } - - return NextResponse.json({ workspace }, { status: 200 }); - } catch (error) { - return NextResponse.json({ message: "Internal Server Error" }, { status: 500 }); - } - } - - try { - const workspaces = await Workspace.find({ owner: userId }) - .populate({ - path: "groups", - select: "name token", - model: Group, - }); - - if (!workspaces || workspaces.length === 0) { - return NextResponse.json({ workspaces: [], message: "No workspaces found." }, { status: 200 }); - } - - return NextResponse.json({ workspaces }, { status: 200 }); - } catch (error: any) { - return NextResponse.json({ message: error.message }, { status: 500 }); - } -} diff --git a/web-app/src/app/api/2fa/authenticator/route.ts b/web-app/src/app/api/2fa/authenticator/route.ts new file mode 100644 index 0000000..31a6ae8 --- /dev/null +++ b/web-app/src/app/api/2fa/authenticator/route.ts @@ -0,0 +1,19 @@ +import { NextRequest } from "next/server"; +import { requireUser } from "@/lib/auth"; +import { badRequest, json, route } from "@/lib/http"; +import { assertCanRemoveFactor, clearIfNoFactorsLeft } from "@/lib/passkeys"; +import User from "@/models/user"; + +/** Removes the authenticator app (not when it's the last second factor and an org requires 2FA). */ +export const DELETE = route(async (req: NextRequest) => { + const me = await requireUser(req); + const user = await User.findById(me.id).select("totpEnabled passkeyCount"); + if (!user?.totpEnabled) throw badRequest("There's no authenticator app on this account."); + await assertCanRemoveFactor(me.id, user.passkeyCount ?? 0); + await User.updateOne( + { _id: me.id }, + { $set: { totpEnabled: false }, $unset: { totpSecret: 1, totpPendingSecret: 1, totpLastStep: 1 } } + ); + await clearIfNoFactorsLeft(me.id); + return json({ message: "Authenticator app removed." }); +}); diff --git a/web-app/src/app/api/2fa/backup-codes/route.ts b/web-app/src/app/api/2fa/backup-codes/route.ts new file mode 100644 index 0000000..3805683 --- /dev/null +++ b/web-app/src/app/api/2fa/backup-codes/route.ts @@ -0,0 +1,33 @@ +import { NextRequest } from "next/server"; +import { z } from "zod"; +import { requireUser } from "@/lib/auth"; +import { badRequest, json, parseBody, route, HttpError } from "@/lib/http"; +import { generateBackupCodes, verifyTotp } from "@/lib/totp"; +import { totpCodeSchema } from "@/lib/validation"; +import { decrypt } from "@/utils/encryption"; +import { mfaLimiter } from "@/utils/limiter"; +import User from "@/models/user"; + +const schema = z.object({ code: totpCodeSchema }); + +/** Regenerates backup codes (invalidating old ones). Requires a fresh TOTP code. */ +export const POST = route(async (req: NextRequest) => { + const me = await requireUser(req); + const { code } = await parseBody(req, schema); + + try { + await mfaLimiter.consume(`regen:${me.id}`); + } catch { + throw new HttpError(429, "Too many attempts. Wait a few minutes and try again."); + } + + const user = await User.findById(me.id).select("+totpSecret +totpLastStep"); + const step = verifyTotp(decrypt(user.totpSecret), code, user.totpLastStep); + if (step === null) throw badRequest("Invalid or already-used code.", "invalid_code"); + + const { codes, hashes } = generateBackupCodes(); + await User.updateOne({ _id: me.id }, { backupCodes: hashes, totpLastStep: step }); + await mfaLimiter.delete(`regen:${me.id}`); + + return json({ backupCodes: codes }); +}); diff --git a/web-app/src/app/api/2fa/enable/route.ts b/web-app/src/app/api/2fa/enable/route.ts new file mode 100644 index 0000000..ec5f41d --- /dev/null +++ b/web-app/src/app/api/2fa/enable/route.ts @@ -0,0 +1,57 @@ +import { NextRequest } from "next/server"; +import { z } from "zod"; +import { requirePartialUser } from "@/lib/auth"; +import { badRequest, json, parseBody, route, HttpError } from "@/lib/http"; +import { generateBackupCodes, generateMfaTicket, verifyTotp } from "@/lib/totp"; +import { totpCodeSchema } from "@/lib/validation"; +import { decrypt } from "@/utils/encryption"; +import { mfaLimiter } from "@/utils/limiter"; +import User from "@/models/user"; +import { assertCanEnroll, isMfaEnrolled } from "@/lib/passkeys"; + +const schema = z.object({ code: totpCodeSchema }); + +/** Confirms enrollment with a first code; returns one-time backup codes and an MFA ticket. */ +export const POST = route(async (req: NextRequest) => { + const { user, jwt } = await requirePartialUser(req); + const { code } = await parseBody(req, schema); + + const full = await User.findById(user._id).select("+totpPendingSecret totpEnabled passkeyCount"); + if (full.totpEnabled) throw badRequest("An authenticator app is already set up."); + assertCanEnroll(full, jwt); + // Someone adding an app next to their passkeys keeps their current backup codes. + const firstFactor = !isMfaEnrolled(full); + if (!full.totpPendingSecret) throw badRequest("Start setup first.", "no_pending_secret"); + + try { + await mfaLimiter.consume(`enable:${user._id}`); + } catch { + throw new HttpError(429, "Too many attempts. Wait a few minutes and try again."); + } + + const secret = decrypt(full.totpPendingSecret); + const step = verifyTotp(secret, code); + if (step === null) throw badRequest("That code is incorrect. Check your authenticator's time and try again.", "invalid_code"); + + await mfaLimiter.delete(`enable:${user._id}`); + + const { codes, hashes } = generateBackupCodes(); + const ticket = generateMfaTicket(); + + await User.updateOne( + { _id: user._id }, + { + $set: { + totpSecret: full.totpPendingSecret, + totpEnabled: true, + totpLastStep: step, + ...(firstFactor ? { backupCodes: hashes } : {}), + mfaTicketHash: ticket.hash, + mfaTicketExpiry: ticket.expiry, + }, + $unset: { totpPendingSecret: 1 }, + } + ); + + return json({ backupCodes: firstFactor ? codes : null, mfaTicket: ticket.ticket }); +}); diff --git a/web-app/src/app/api/2fa/methods/route.ts b/web-app/src/app/api/2fa/methods/route.ts new file mode 100644 index 0000000..5c8aab2 --- /dev/null +++ b/web-app/src/app/api/2fa/methods/route.ts @@ -0,0 +1,15 @@ +import { NextRequest } from "next/server"; +import { requirePartialUser } from "@/lib/auth"; +import { json, route } from "@/lib/http"; +import User from "@/models/user"; + +/** Which second factors the signed-in user has, so the sign-in step can offer the right ones. */ +export const GET = route(async (req: NextRequest) => { + const { user } = await requirePartialUser(req); + const full = await User.findById(user._id).select("totpEnabled passkeyCount +backupCodes"); + return json({ + totp: !!full?.totpEnabled, + passkeys: full?.passkeyCount ?? 0, + backupCodes: full?.backupCodes?.length ?? 0, + }); +}); diff --git a/web-app/src/app/api/2fa/reveal/route.ts b/web-app/src/app/api/2fa/reveal/route.ts new file mode 100644 index 0000000..982772b --- /dev/null +++ b/web-app/src/app/api/2fa/reveal/route.ts @@ -0,0 +1,55 @@ +import { NextRequest } from "next/server"; +import { z } from "zod"; +import { requireUser } from "@/lib/auth"; +import { badRequest, forbidden, HttpError, json, parseBody, route } from "@/lib/http"; +import { verifyTotp } from "@/lib/totp"; +import { totpCodeSchema } from "@/lib/validation"; +import { REVEAL_TTL_MS, revealUntil } from "@/lib/reveal"; +import { decrypt } from "@/utils/encryption"; +import { mfaLimiter } from "@/utils/limiter"; +import User from "@/models/user"; + +const schema = z.object({ code: totpCodeSchema }); + +/** Whether secret values are unlocked for this user, and until when. */ +export const GET = route(async (req: NextRequest) => { + const me = await requireUser(req); + const until = await revealUntil(me); + return json({ revealUntil: until }); +}); + +/** + * Unlocks secret values for 10 minutes after a fresh authenticator code. Same replay guard and rate limit + * as sign-in: a code can't be reused, and 5 wrong tries lock it for 5 minutes. + */ +export const POST = route(async (req: NextRequest) => { + const me = await requireUser(req); + if (me.via !== "session") throw forbidden("Unlocking values needs a signed-in session."); + const { code } = await parseBody(req, schema); + + const full = await User.findById(me.id).select("+totpSecret +totpLastStep totpEnabled"); + if (!full?.totpEnabled || !full.totpSecret) throw badRequest("Two-factor authentication is not set up.", "not_enrolled"); + + const limiterKey = `reveal:${me.id}`; + try { + await mfaLimiter.consume(limiterKey); + } catch (e: any) { + const secs = Math.ceil((e?.msBeforeNext ?? 300000) / 1000); + throw new HttpError(429, `Too many attempts. Try again in ${Math.ceil(secs / 60)} min.`); + } + + const step = verifyTotp(decrypt(full.totpSecret), code, full.totpLastStep); + if (step === null) throw badRequest("That code is incorrect or was already used. Wait for the next one.", "invalid_code"); + await mfaLimiter.delete(limiterKey); + + const until = new Date(Date.now() + REVEAL_TTL_MS); + await User.updateOne({ _id: me.id }, { $set: { totpLastStep: step, revealUntil: until } }); + return json({ revealUntil: until }); +}); + +/** Locks values again right away. */ +export const DELETE = route(async (req: NextRequest) => { + const me = await requireUser(req); + await User.updateOne({ _id: me.id }, { $unset: { revealUntil: 1 } }); + return json({ revealUntil: null }); +}); diff --git a/web-app/src/app/api/2fa/setup/route.ts b/web-app/src/app/api/2fa/setup/route.ts new file mode 100644 index 0000000..737af13 --- /dev/null +++ b/web-app/src/app/api/2fa/setup/route.ts @@ -0,0 +1,20 @@ +import { NextRequest } from "next/server"; +import { requirePartialUser } from "@/lib/auth"; +import { badRequest, json, route } from "@/lib/http"; +import { generateTotpSecret, totpQrDataUrl, totpUri } from "@/lib/totp"; +import { encrypt } from "@/utils/encryption"; +import { assertCanEnroll } from "@/lib/passkeys"; +import User from "@/models/user"; + +/** Starts enrollment: creates a pending secret and returns it as a QR code + manual key. */ +export const POST = route(async (req: NextRequest) => { + const { user, jwt } = await requirePartialUser(req); + if (user.totpEnabled) throw badRequest("An authenticator app is already set up."); + assertCanEnroll(user, jwt); + + const secret = generateTotpSecret(); + await User.updateOne({ _id: user._id }, { totpPendingSecret: encrypt(secret) }); + + const uri = totpUri(secret, user.email); + return json({ secret, uri, qr: await totpQrDataUrl(uri) }); +}); diff --git a/web-app/src/app/api/2fa/verify/route.ts b/web-app/src/app/api/2fa/verify/route.ts new file mode 100644 index 0000000..e1e5bfa --- /dev/null +++ b/web-app/src/app/api/2fa/verify/route.ts @@ -0,0 +1,59 @@ +import { NextRequest } from "next/server"; +import { z } from "zod"; +import { requirePartialUser } from "@/lib/auth"; +import { badRequest, json, parseBody, route, HttpError } from "@/lib/http"; +import { consumeBackupCode, generateMfaTicket, verifyTotp } from "@/lib/totp"; +import { totpCodeSchema } from "@/lib/validation"; +import { decrypt } from "@/utils/encryption"; +import { mfaLimiter } from "@/utils/limiter"; +import User from "@/models/user"; +import { isMfaEnrolled } from "@/lib/passkeys"; + +const schema = z.union([ + z.object({ code: totpCodeSchema }), + z.object({ backupCode: z.string().trim().min(8).max(32) }), +]); + +/** Login challenge: verify a TOTP or backup code and mint a one-time MFA ticket. */ +export const POST = route(async (req: NextRequest) => { + const { user } = await requirePartialUser(req); + const body = await parseBody(req, schema); + + const full = await User.findById(user._id).select("+totpSecret +totpLastStep +backupCodes totpEnabled passkeyCount"); + if (!isMfaEnrolled(full)) throw badRequest("Two-factor authentication is not set up.", "not_enrolled"); + if ("code" in body && (!full.totpEnabled || !full.totpSecret)) { + throw badRequest("There's no authenticator app on this account. Use your passkey or a backup code.", "no_totp"); + } + + const limiterKey = `verify:${user._id}`; + try { + await mfaLimiter.consume(limiterKey); + } catch (e: any) { + const secs = Math.ceil((e?.msBeforeNext ?? 300000) / 1000); + throw new HttpError(429, `Too many attempts. Try again in ${Math.ceil(secs / 60)} min.`); + } + + const update: Record = {}; + let backupCodesRemaining: number | undefined; + + if ("code" in body) { + const step = verifyTotp(decrypt(full.totpSecret), body.code, full.totpLastStep); + if (step === null) throw badRequest("Invalid or already-used code.", "invalid_code"); + update.totpLastStep = step; + } else { + const remaining = consumeBackupCode(full.backupCodes ?? [], body.backupCode); + if (!remaining) throw badRequest("Invalid backup code.", "invalid_code"); + update.backupCodes = remaining; + backupCodesRemaining = remaining.length; + } + + await mfaLimiter.delete(limiterKey); + + const ticket = generateMfaTicket(); + await User.updateOne( + { _id: user._id }, + { $set: { ...update, mfaTicketHash: ticket.hash, mfaTicketExpiry: ticket.expiry } } + ); + + return json({ mfaTicket: ticket.ticket, backupCodesRemaining }); +}); diff --git a/web-app/src/app/api/[...path]/route.ts b/web-app/src/app/api/[...path]/route.ts new file mode 100644 index 0000000..6dd70f5 --- /dev/null +++ b/web-app/src/app/api/[...path]/route.ts @@ -0,0 +1,10 @@ +import { json } from "@/lib/http"; + +/** Any /api path without its own route: answer in JSON like the rest of the API, not with the HTML 404 page. */ +const notFound = () => json({ message: "This API endpoint doesn't exist.", code: "not_found" }, 404); + +export const GET = notFound; +export const POST = notFound; +export const PUT = notFound; +export const PATCH = notFound; +export const DELETE = notFound; diff --git a/web-app/src/app/api/account/password/route.ts b/web-app/src/app/api/account/password/route.ts new file mode 100644 index 0000000..cfeda6f --- /dev/null +++ b/web-app/src/app/api/account/password/route.ts @@ -0,0 +1,33 @@ +import { NextRequest } from "next/server"; +import { z } from "zod"; +import { requireUser } from "@/lib/auth"; +import { badRequest, json, parseBody, route } from "@/lib/http"; +import { passwordSchema } from "@/lib/validation"; +import { comparePassword, hashPassword } from "@/utils/bcrypt"; +import User from "@/models/user"; + +const schema = z.object({ currentPassword: z.string().optional(), newPassword: passwordSchema }); + +/** + * Sets a first password (accounts start with Google sign-in only), after which email + password sign-in + * works too; or changes it, which signs out every session including this one. + */ +export const POST = route(async (req: NextRequest) => { + const me = await requireUser(req); + const { currentPassword, newPassword } = await parseBody(req, schema); + + const user = await User.findById(me.id).select("+password provider"); + if (!user.password) { + await User.updateOne({ _id: me.id }, { $set: { password: await hashPassword(newPassword) } }); + return json({ message: "Password set. You can now sign in with your email and password too.", signedOut: false }); + } + if (!currentPassword || !(await comparePassword(currentPassword, user.password))) { + throw badRequest("Current password is incorrect.", "invalid_password"); + } + + await User.updateOne( + { _id: me.id }, + { $set: { password: await hashPassword(newPassword) }, $inc: { sessionVersion: 1 }, $unset: { revealUntil: 1 } } + ); + return json({ message: "Password changed. Please sign in again.", signedOut: true }); +}); diff --git a/web-app/src/app/api/account/sessions/route.ts b/web-app/src/app/api/account/sessions/route.ts new file mode 100644 index 0000000..cf78a08 --- /dev/null +++ b/web-app/src/app/api/account/sessions/route.ts @@ -0,0 +1,11 @@ +import { NextRequest } from "next/server"; +import { requireUser } from "@/lib/auth"; +import { json, route } from "@/lib/http"; +import User from "@/models/user"; + +/** Signs out every session for this account, everywhere. */ +export const DELETE = route(async (req: NextRequest) => { + const me = await requireUser(req); + await User.updateOne({ _id: me.id }, { $inc: { sessionVersion: 1 }, $unset: { revealUntil: 1 } }); + return json({ message: "Signed out of all sessions." }); +}); diff --git a/web-app/src/app/api/env/get/route.ts b/web-app/src/app/api/env/get/route.ts new file mode 100644 index 0000000..f52add1 --- /dev/null +++ b/web-app/src/app/api/env/get/route.ts @@ -0,0 +1,16 @@ +import { NextResponse } from "next/server"; + +/** + * Legacy group-token endpoint. Group tokens were replaced by personal access tokens, + * which respect each user's org/team role. See README → "CLI & API access". + */ +export function GET() { + return NextResponse.json( + { + message: + "This endpoint was removed. Create a personal access token in Account → API tokens and call GET /api/v1/{org}/{team}/{project}/{environment}.", + code: "endpoint_removed", + }, + { status: 410 } + ); +} diff --git a/web-app/src/app/api/invites/accept/route.ts b/web-app/src/app/api/invites/accept/route.ts new file mode 100644 index 0000000..0015ad7 --- /dev/null +++ b/web-app/src/app/api/invites/accept/route.ts @@ -0,0 +1,70 @@ +import { NextRequest } from "next/server"; +import { z } from "zod"; +import Invite from "@/models/invite"; +import OrgMembership from "@/models/orgMembership"; +import TeamMembership from "@/models/teamMembership"; +import Team from "@/models/team"; +import User from "@/models/user"; +import Organization from "@/models/organization"; +import { requireUser } from "@/lib/auth"; +import { audit } from "@/lib/audit"; +import { badRequest, conflict, json, notFound, parseBody, rateLimit, route } from "@/lib/http"; +import { sha256 } from "@/lib/totp"; +import { normalizeInviteToken } from "@/lib/invite-code"; + +const schema = z.object({ token: z.string().trim().min(10).max(256) }); + +/** Accepts an invite by its code (or the link that contains it). */ +export const POST = route(async (req: NextRequest) => { + const me = await requireUser(req); + const body = await parseBody(req, schema); + await rateLimit(req, `invite-accept:${me.id}`); + + // Links only: invites created before links-only (in-app email invites) can't be accepted. + const invite: any = await Invite.findOne({ tokenHash: sha256(normalizeInviteToken(body.token)), kind: "link" }); + if (!invite) throw notFound("That invite code isn't valid. Check it and try again, or ask an admin for a new one."); + + if (invite.revokedAt) throw badRequest("This invite was revoked.", "invite_revoked"); + if (invite.expiresAt.getTime() < Date.now()) throw badRequest("This invite has expired. Ask for a new one.", "invite_expired"); + if (invite.maxUses && invite.uses >= invite.maxUses) throw badRequest("This invite has already been used.", "invite_used"); + + const existing = await OrgMembership.findOne({ org: invite.org, user: me.id }); + const team = invite.team ? await Team.findOne({ _id: invite.team, org: invite.org }) : null; + + if (existing && (!team || (await TeamMembership.exists({ team: team._id, user: me.id })))) { + throw conflict("You're already a member of this organization.", "already_member"); + } + + // Atomically claim a use (compare-and-swap on the use count we just checked), so a link + // with maxUses can't be over-redeemed by concurrent requests. + const claimed = await Invite.findOneAndUpdate( + { _id: invite._id, revokedAt: null, uses: invite.uses }, + { $inc: { uses: 1 }, $addToSet: { acceptedBy: me.id } }, + { new: true } + ); + if (!claimed) throw badRequest("This invite has already been used.", "invite_used"); + + if (!existing) { + await OrgMembership.create({ org: invite.org, user: me.id, role: invite.orgRole || "member" }); + } + if (team) { + await TeamMembership.updateOne( + { team: team._id, user: me.id }, + { $setOnInsert: { team: team._id, org: invite.org, user: me.id, role: invite.teamRole || "viewer" } }, + { upsert: true } + ); + } + + const actor = { id: me.id, name: me.name, email: me.email }; + await audit(String(invite.org), actor, existing ? "invite.accept" : "member.join", { type: "user", id: me.id, name: me.email }, { + via: invite.kind, + orgRole: existing ? undefined : invite.orgRole, + team: team?.name, + teamRole: team ? invite.teamRole : undefined, + }, req); + + const org = await Organization.findById(invite.org).select("slug name"); + await User.updateOne({ _id: me.id }, { lastOrgSlug: org?.slug }); + + return json({ org: { slug: org?.slug, name: org?.name }, team: team ? { slug: team.slug, name: team.name } : null }); +}); diff --git a/web-app/src/app/api/invites/preview/[token]/route.ts b/web-app/src/app/api/invites/preview/[token]/route.ts new file mode 100644 index 0000000..ff507e9 --- /dev/null +++ b/web-app/src/app/api/invites/preview/[token]/route.ts @@ -0,0 +1,35 @@ +import { NextRequest } from "next/server"; +import Invite from "@/models/invite"; +import { json, notFound, rateLimit, route } from "@/lib/http"; +import { sha256 } from "@/lib/totp"; +import { normalizeInviteToken } from "@/lib/invite-code"; + +/** Public, minimal preview of a link invite so the join page can show where it leads. */ +export const GET = route<{ token: string }>(async (req: NextRequest, { params }) => { + await rateLimit(req); + const invite: any = await Invite.findOne({ tokenHash: sha256(normalizeInviteToken(decodeURIComponent(params.token))), kind: "link" }) + .populate("org", "name") + .populate("team", "name") + .populate("createdBy", "name"); + if (!invite || !invite.org) throw notFound("This invite link is invalid."); + + const status = invite.revokedAt + ? "revoked" + : invite.expiresAt.getTime() < Date.now() + ? "expired" + : invite.maxUses && invite.uses >= invite.maxUses + ? "used_up" + : "active"; + + return json({ + invite: { + org: invite.org.name, + team: invite.team?.name ?? null, + orgRole: invite.orgRole, + teamRole: invite.teamRole ?? null, + invitedBy: invite.createdBy?.name ?? null, + expiresAt: invite.expiresAt, + status, + }, + }); +}); diff --git a/web-app/src/app/api/me/route.ts b/web-app/src/app/api/me/route.ts new file mode 100644 index 0000000..85dff9f --- /dev/null +++ b/web-app/src/app/api/me/route.ts @@ -0,0 +1,39 @@ +import { NextRequest } from "next/server"; +import { z } from "zod"; +import User from "@/models/user"; +import ApiToken from "@/models/apiToken"; +import { requireUser } from "@/lib/auth"; +import { json, parseBody, route } from "@/lib/http"; +import { nameSchema } from "@/lib/validation"; + +export const GET = route(async (req: NextRequest) => { + const me = await requireUser(req); + const user = await User.findById(me.id).select("name email username image provider emailVerified totpEnabled lastOrgSlug createdAt +backupCodes +password"); + return json({ + user: { + id: me.id, + name: user.name, + email: user.email, + image: user.image ?? null, + username: user.username, + provider: user.provider, + emailVerified: !!user.emailVerified, + totpEnabled: !!user.totpEnabled, + // Only whether one is set; the hash never leaves the server. + hasPassword: !!user.password, + backupCodesRemaining: user.backupCodes?.length ?? 0, + lastOrgSlug: user.lastOrgSlug ?? null, + createdAt: user.createdAt, + apiTokens: await ApiToken.countDocuments({ user: me.id, revokedAt: null }), + }, + }); +}); + +const patchSchema = z.object({ name: nameSchema.optional(), lastOrgSlug: z.string().max(64).optional() }); + +export const PATCH = route(async (req: NextRequest) => { + const me = await requireUser(req); + const body = await parseBody(req, patchSchema); + await User.updateOne({ _id: me.id }, { $set: body }); + return json({ message: "Profile updated." }); +}); diff --git a/web-app/src/app/api/orgs/[org]/audit/route.ts b/web-app/src/app/api/orgs/[org]/audit/route.ts new file mode 100644 index 0000000..bfa7b75 --- /dev/null +++ b/web-app/src/app/api/orgs/[org]/audit/route.ts @@ -0,0 +1,50 @@ +import { NextRequest } from "next/server"; +import { Types, trusted } from "mongoose"; +import AuditLog from "@/models/auditLog"; +import { requireUser } from "@/lib/auth"; +import { assertOrgAdmin, loadOrg } from "@/lib/authz"; +import { json, route } from "@/lib/http"; +import { afterFilter, readPage, sortSpec, toPage, type SortField } from "@/lib/pagination"; + +const AUDIT_SORT: SortField[] = [{ key: "_id", dir: -1, type: "id" }]; + +/** Owner/admin only. Paginated like every list (newest first), filterable by action prefix, actor and text. */ +export const GET = route<{ org: string }>(async (req: NextRequest, { params }) => { + const me = await requireUser(req); + const { org, role } = await loadOrg(params.org, me); + assertOrgAdmin(role); + + const { limit, after } = readPage(req, AUDIT_SORT); + const sp = req.nextUrl.searchParams; + const filter: Record = { org: org._id }; + + const action = sp.get("action"); + if (action) filter.action = trusted({ $regex: `^${action.replace(/[^a-z0-9_.]/gi, "")}` }); + + const actor = sp.get("actor"); + if (actor && Types.ObjectId.isValid(actor)) filter.actor = new Types.ObjectId(actor); + + const q = sp.get("q")?.trim(); + if (q) { + const safe = q.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").slice(0, 64); + filter.$or = [{ actorEmail: trusted({ $regex: safe, $options: "i" }) }, { "target.name": trusted({ $regex: safe, $options: "i" }) }]; + } + + // The text search uses $or too, so combine it with the cursor condition through $and. + const query = after ? { $and: [filter, afterFilter(AUDIT_SORT, after)] } : filter; + const rows = await AuditLog.find(query).sort(sortSpec(AUDIT_SORT)).limit(limit + 1).lean(); + const { items: page, nextCursor } = toPage(rows as any[], limit, (e) => [e._id]); + + return json({ + entries: page.map((e: any) => ({ + id: e._id, + action: e.action, + actor: e.actor ? { id: e.actor, name: e.actorName, email: e.actorEmail } : null, + target: e.target ?? null, + meta: e.meta ?? null, + ip: e.ip ?? null, + createdAt: e.createdAt, + })), + nextCursor, + }); +}); diff --git a/web-app/src/app/api/orgs/[org]/invites/[id]/route.ts b/web-app/src/app/api/orgs/[org]/invites/[id]/route.ts new file mode 100644 index 0000000..7a19bc9 --- /dev/null +++ b/web-app/src/app/api/orgs/[org]/invites/[id]/route.ts @@ -0,0 +1,21 @@ +import { NextRequest } from "next/server"; +import Invite from "@/models/invite"; +import { requireUser } from "@/lib/auth"; +import { audit } from "@/lib/audit"; +import { assertOrgAdmin, loadOrg } from "@/lib/authz"; +import { json, notFound, route } from "@/lib/http"; + +export const DELETE = route<{ org: string; id: string }>(async (req: NextRequest, { params }) => { + const me = await requireUser(req); + const { org, role } = await loadOrg(params.org, me); + assertOrgAdmin(role); + + const invite = await Invite.findOne({ _id: params.id, org: org._id }); + if (!invite) throw notFound("Invite not found."); + if (!invite.revokedAt) { + invite.revokedAt = new Date(); + await invite.save(); + await audit(org, me, "invite.revoke", { type: "invite", id: String(invite._id), name: invite.email ?? "invite link" }, undefined, req); + } + return json({ message: "Invite revoked." }); +}); diff --git a/web-app/src/app/api/orgs/[org]/invites/route.ts b/web-app/src/app/api/orgs/[org]/invites/route.ts new file mode 100644 index 0000000..75dc157 --- /dev/null +++ b/web-app/src/app/api/orgs/[org]/invites/route.ts @@ -0,0 +1,125 @@ +import { NextRequest } from "next/server"; +import { z } from "zod"; +import { randomInt } from "crypto"; +import Invite from "@/models/invite"; +import Team from "@/models/team"; +import { requireUser } from "@/lib/auth"; +import { audit } from "@/lib/audit"; +import { assertOrgAdmin, loadOrg } from "@/lib/authz"; +import { badRequest, forbidden, json, parseBody, route } from "@/lib/http"; +import { orgRoleSchema, teamRoleSchema } from "@/lib/validation"; +import { sha256 } from "@/lib/totp"; +import { afterFilter, readPage, sortSpec, toPage, type SortField } from "@/lib/pagination"; +import { INVITE_CODE_ALPHABET, INVITE_CODE_LENGTH } from "@/lib/invite-code"; + +/** A fresh 10-character invite code that no other invite uses. */ +async function newInviteCode() { + for (;;) { + let code = ""; + for (let i = 0; i < INVITE_CODE_LENGTH; i++) code += INVITE_CODE_ALPHABET[randomInt(INVITE_CODE_ALPHABET.length)]; + if (!(await Invite.exists({ tokenHash: sha256(code) }))) return code; + } +} + +type P = { org: string }; + +function serialize(invite: any) { + const now = Date.now(); + const status = invite.revokedAt + ? "revoked" + : invite.expiresAt.getTime() < now + ? "expired" + : invite.maxUses && invite.uses >= invite.maxUses + ? invite.kind === "email" ? "accepted" : "used_up" + : "active"; + return { + id: invite._id, + kind: invite.kind, + email: invite.email ?? null, + orgRole: invite.orgRole, + team: invite.team ? { name: invite.team.name, slug: invite.team.slug } : null, + teamRole: invite.teamRole ?? null, + expiresAt: invite.expiresAt, + maxUses: invite.maxUses ?? null, + uses: invite.uses, + createdBy: invite.createdBy?.name ?? null, + createdAt: invite.createdAt, + status, + }; +} + +const INVITE_SORT: SortField[] = [{ key: "_id", dir: -1, type: "id" }]; + +/** The org's invites, newest first. Owners and admins only. Paginated. */ +export const GET = route

(async (req: NextRequest, { params }) => { + const me = await requireUser(req); + const { org, role } = await loadOrg(params.org, me); + assertOrgAdmin(role); + const { limit, after } = readPage(req, INVITE_SORT); + const rows = await Invite.find({ org: org._id, ...afterFilter(INVITE_SORT, after) }) + .populate("team", "name slug") + .populate("createdBy", "name") + .sort(sortSpec(INVITE_SORT)) + .limit(limit + 1); + const { items, nextCursor } = toPage(rows as any[], limit, (i) => [i._id]); + return json({ invites: items.map(serialize), nextCursor }); +}); + +// Invitations are shareable links only; Envoy doesn't send email. +const schema = z + .object({ + kind: z.literal("link").default("link"), + orgRole: orgRoleSchema.default("member"), + teamSlug: z.string().optional(), + teamRole: teamRoleSchema.optional(), + expiresInDays: z.number().int().min(1).max(30).default(7), + /** A custom expiry instead of a preset: within the next 30 days. */ + expiresAt: z.iso.datetime({ offset: true }).optional(), + maxUses: z.number().int().min(1).max(1000).optional(), + }) + .refine((v) => !v.teamSlug || !!v.teamRole, { message: "Pick a team role.", path: ["teamRole"] }) + .refine((v) => !v.expiresAt || (Date.parse(v.expiresAt) > Date.now() && Date.parse(v.expiresAt) <= Date.now() + 31 * 24 * 60 * 60 * 1000), { + message: "Pick an expiry date between tomorrow and 30 days from now.", + path: ["expiresAt"], + }); + +/** Creates an invite. Its 10-character code (also used in the /invite/ link) is returned once, in the response. */ +export const POST = route

(async (req: NextRequest, { params }) => { + const me = await requireUser(req); + const { org, role } = await loadOrg(params.org, me); + assertOrgAdmin(role); + const body = await parseBody(req, schema); + + if (body.orgRole === "admin" && role !== "owner") throw forbidden("Only the owner can invite admins."); + + let team = null; + if (body.teamSlug) { + team = await Team.findOne({ org: org._id, slug: body.teamSlug }); + if (!team) throw badRequest("Team not found."); + } + + const token = await newInviteCode(); + const invite = await Invite.create({ + org: org._id, + kind: "link", + tokenHash: sha256(token), + orgRole: body.orgRole, + team: team?._id, + teamRole: team ? body.teamRole : undefined, + expiresAt: body.expiresAt ? new Date(body.expiresAt) : new Date(Date.now() + body.expiresInDays * 24 * 60 * 60 * 1000), + maxUses: body.maxUses, + createdBy: me.id, + }); + + await audit( + org, + me, + "invite.create", + { type: "invite", id: String(invite._id), name: "invite link" }, + { orgRole: body.orgRole, team: team?.name, teamRole: body.teamRole, expiresInDays: body.expiresAt ? undefined : body.expiresInDays, expiresAt: body.expiresAt, maxUses: body.maxUses }, + req + ); + + await invite.populate("team", "name slug"); + return json({ invite: serialize(invite), token }, 201); +}); diff --git a/web-app/src/app/api/orgs/[org]/members/[userId]/reset-2fa/route.ts b/web-app/src/app/api/orgs/[org]/members/[userId]/reset-2fa/route.ts new file mode 100644 index 0000000..2de3d84 --- /dev/null +++ b/web-app/src/app/api/orgs/[org]/members/[userId]/reset-2fa/route.ts @@ -0,0 +1,41 @@ +import { NextRequest } from "next/server"; +import OrgMembership from "@/models/orgMembership"; +import User from "@/models/user"; +import Passkey from "@/models/passkey"; +import { requireUser } from "@/lib/auth"; +import { audit } from "@/lib/audit"; +import { assertOrgOwner, loadOrg } from "@/lib/authz"; +import { badRequest, forbidden, json, notFound, route } from "@/lib/http"; + +/** + * Owner-only recovery for a member who lost their authenticator and backup codes. + * Clears their authenticator app and passkeys, revokes all their sessions; they re-enroll after their next password/Google sign-in. + */ +export const POST = route<{ org: string; userId: string }>(async (req: NextRequest, { params }) => { + const me = await requireUser(req); + const { org, role } = await loadOrg(params.org, me); + assertOrgOwner(role); + + if (params.userId === me.id) throw badRequest("You can't reset your own 2FA. Use a backup code instead."); + const target = await OrgMembership.findOne({ org: org._id, user: params.userId }).populate("user", "name email"); + if (!target) throw notFound("Member not found."); + + // An owner of another org is outside this owner's authority. + if (await OrgMembership.exists({ user: params.userId, role: "owner" })) { + throw forbidden("This person owns another organization; their 2FA can't be reset from here."); + } + + await User.updateOne( + { _id: params.userId }, + { + $set: { totpEnabled: false, backupCodes: [], passkeyCount: 0 }, + $unset: { totpSecret: 1, totpPendingSecret: 1, totpLastStep: 1, mfaTicketHash: 1, mfaTicketExpiry: 1, revealUntil: 1 }, + $inc: { sessionVersion: 1 }, + } + ); + + await Passkey.deleteMany({ user: params.userId }); + + await audit(org, me, "member.2fa_reset", { type: "user", id: params.userId, name: target.user?.email }, undefined, req); + return json({ message: `2FA reset for ${target.user?.name}. They'll set it up again at next sign-in.` }); +}); diff --git a/web-app/src/app/api/orgs/[org]/members/[userId]/route.ts b/web-app/src/app/api/orgs/[org]/members/[userId]/route.ts new file mode 100644 index 0000000..1e67192 --- /dev/null +++ b/web-app/src/app/api/orgs/[org]/members/[userId]/route.ts @@ -0,0 +1,73 @@ +import { NextRequest } from "next/server"; +import { z } from "zod"; +import OrgMembership from "@/models/orgMembership"; +import TeamMembership from "@/models/teamMembership"; +import Project from "@/models/project"; +import { requireUser } from "@/lib/auth"; +import { audit } from "@/lib/audit"; +import { assertOrgAdmin, loadOrg } from "@/lib/authz"; +import { badRequest, forbidden, json, notFound, parseBody, route } from "@/lib/http"; +import { orgRoleSchema } from "@/lib/validation"; + +type P = { org: string; userId: string }; + +const schema = z.object({ role: orgRoleSchema }); + +/** Change a member's org role. Only the owner can grant/revoke admin. */ +export const PATCH = route

(async (req: NextRequest, { params }) => { + const me = await requireUser(req); + const { org, role } = await loadOrg(params.org, me); + assertOrgAdmin(role); + const { role: newRole } = await parseBody(req, schema); + + if (params.userId === me.id) throw badRequest("You can't change your own role."); + const target = await OrgMembership.findOne({ org: org._id, user: params.userId }).populate("user", "name email"); + if (!target) throw notFound("Member not found."); + if (target.role === "owner") throw forbidden("Use ownership transfer to change the owner's role."); + if ((target.role === "admin" || newRole === "admin") && role !== "owner") { + throw forbidden("Only the owner can grant or remove admin access."); + } + if (target.role === newRole) return json({ message: "No change." }); + + const from = target.role; + target.role = newRole; + await target.save(); + await audit(org, me, "member.role_change", { type: "user", id: params.userId, name: target.user?.email }, { from, to: newRole }, req); + + return json({ message: "Role updated." }); +}); + +/** Remove a member (admin+) or leave the org (`userId = me`). Cascades team memberships. */ +export const DELETE = route

(async (req: NextRequest, { params }) => { + const me = await requireUser(req); + const leaving = params.userId === "me" || params.userId === me.id; + // Leaving never needs the org's 2FA requirement to be met. + const { org, role } = await loadOrg(params.org, me, { skip2fa: leaving }); + const targetId = leaving ? me.id : params.userId; + + const target = await OrgMembership.findOne({ org: org._id, user: targetId }).populate("user", "name email"); + if (!target) throw notFound("Member not found."); + + if (leaving) { + if (role === "owner") throw badRequest("Transfer ownership before leaving the organization.", "owner_cannot_leave"); + } else { + assertOrgAdmin(role); + if (target.role === "owner") throw forbidden("The owner can't be removed."); + if (target.role === "admin" && role !== "owner") throw forbidden("Only the owner can remove admins."); + } + + await TeamMembership.deleteMany({ org: org._id, user: targetId }); + // Drop them from every "Only selected people" list in the org, so rejoining doesn't restore access. + await Project.updateMany({ org: org._id, allowedUsers: targetId }, { $pull: { allowedUsers: targetId } }, { timestamps: false }); + await OrgMembership.deleteOne({ _id: target._id }); + await audit( + org, + me, + leaving ? "member.leave" : "member.remove", + { type: "user", id: targetId, name: target.user?.email }, + undefined, + req + ); + + return json({ message: leaving ? "You left the organization." : "Member removed." }); +}); diff --git a/web-app/src/app/api/orgs/[org]/members/route.ts b/web-app/src/app/api/orgs/[org]/members/route.ts new file mode 100644 index 0000000..a3c73d4 --- /dev/null +++ b/web-app/src/app/api/orgs/[org]/members/route.ts @@ -0,0 +1,81 @@ +import { NextRequest } from "next/server"; +import OrgMembership from "@/models/orgMembership"; +import TeamMembership from "@/models/teamMembership"; +import User from "@/models/user"; +import { requireUser } from "@/lib/auth"; +import { loadOrg } from "@/lib/authz"; +import { json, route } from "@/lib/http"; +import { afterMatch, readPage, toPage, type SortField } from "@/lib/pagination"; +import { trusted } from "mongoose"; + +/** Owners first, then admins, then members; by name within a role. */ +const MEMBER_SORT: SortField[] = [ + { key: "rank", dir: 1, type: "number" }, + { key: "nameKey", dir: 1, type: "string" }, + { key: "_id", dir: 1, type: "id" }, +]; + +const escapeRegex = (s: string) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + +/** + * Organization members, paginated. `?q=` filters by name or email (case-insensitive). + * `total` is the number of members matching `q` (or all members without it). + */ +export const GET = route<{ org: string }>(async (req: NextRequest, { params }) => { + const me = await requireUser(req); + const { org } = await loadOrg(params.org, me); + const { limit, after } = readPage(req, MEMBER_SORT); + const q = req.nextUrl.searchParams.get("q")?.trim().slice(0, 100) ?? ""; + + const base = [ + { $match: { org: org._id } }, + { $lookup: { from: User.collection.name, localField: "user", foreignField: "_id", as: "u" } }, + { $unwind: "$u" }, + ...(q ? [{ $match: { $or: [{ "u.name": { $regex: escapeRegex(q), $options: "i" } }, { "u.email": { $regex: escapeRegex(q), $options: "i" } }] } }] : []), + ]; + + const [rows, counted] = await Promise.all([ + OrgMembership.aggregate([ + ...base, + { + $addFields: { + rank: { $switch: { branches: [{ case: { $eq: ["$role", "owner"] }, then: 0 }, { case: { $eq: ["$role", "admin"] }, then: 1 }], default: 2 } }, + nameKey: { $toLower: "$u.name" }, + }, + }, + { $match: afterMatch(MEMBER_SORT, after) }, + { $sort: { rank: 1, nameKey: 1, _id: 1 } }, + { $limit: limit + 1 }, + ]), + OrgMembership.aggregate([...base, { $count: "n" }]), + ]); + const { items, nextCursor } = toPage(rows as any[], limit, (m) => [m.rank, m.nameKey, m._id]); + + // Team memberships for this page's members only. + const teamMemberships = await TeamMembership.find({ org: org._id, user: trusted({ $in: items.map((m) => m.u._id) }) }).populate("team", "name slug"); + const teamsByUser = new Map(); + for (const tm of teamMemberships as any[]) { + if (!tm.team) continue; + const list = teamsByUser.get(String(tm.user)) ?? []; + list.push({ name: tm.team.name, slug: tm.team.slug, role: tm.role }); + teamsByUser.set(String(tm.user), list); + } + + return json({ + total: counted[0]?.n ?? 0, + nextCursor, + members: items.map((m) => ({ + userId: m.u._id, + name: m.u.name, + email: m.u.email, + image: m.u.image ?? null, + provider: m.u.provider, + // Has a second factor: an authenticator app or a passkey. + totpEnabled: !!m.u.totpEnabled || (m.u.passkeyCount ?? 0) > 0, + role: m.role, + joinedAt: m.createdAt, + teams: teamsByUser.get(String(m.u._id)) ?? [], + isMe: String(m.u._id) === me.id, + })), + }); +}); diff --git a/web-app/src/app/api/orgs/[org]/route.ts b/web-app/src/app/api/orgs/[org]/route.ts new file mode 100644 index 0000000..2f28a21 --- /dev/null +++ b/web-app/src/app/api/orgs/[org]/route.ts @@ -0,0 +1,100 @@ +import { NextRequest } from "next/server"; +import { z } from "zod"; +import Organization from "@/models/organization"; +import OrgMembership from "@/models/orgMembership"; +import Team from "@/models/team"; +import TeamMembership from "@/models/teamMembership"; +import Project from "@/models/project"; +import Environment from "@/models/environment"; +import Invite from "@/models/invite"; +import AuditLog from "@/models/auditLog"; +import { requireUser } from "@/lib/auth"; +import { audit } from "@/lib/audit"; +import { assertOrgAdmin, assertOrgOwner, isOrgAdmin, loadOrg, visibleProjectsFilter, visibleTeamIds } from "@/lib/authz"; +import { badRequest, conflict, json, parseBody, route } from "@/lib/http"; +import { nameSchema, slugSchema } from "@/lib/validation"; +import { trusted } from "mongoose"; + +type P = { org: string }; + +/** Org overview + the navigation tree (teams → projects) visible to the caller. */ +export const GET = route

(async (req: NextRequest, { params }) => { + const me = await requireUser(req); + const { org, role } = await loadOrg(params.org, me); + + const teamIds = await visibleTeamIds(org._id, role, me.id); + const [teams, projects, myTeamRoles, memberCount] = await Promise.all([ + Team.find({ _id: trusted({ $in: teamIds }) }).sort({ name: 1 }), + Project.find({ team: trusted({ $in: teamIds }), ...visibleProjectsFilter(role, me.id) }) + .select("name slug team updatedAt restricted") + .sort({ name: 1 }), + TeamMembership.find({ org: org._id, user: me.id }).select("team role"), + OrgMembership.countDocuments({ org: org._id }), + ]); + const memberCounts = await TeamMembership.aggregate([ + { $match: { team: { $in: teamIds } } }, + { $group: { _id: "$team", count: { $sum: 1 } } }, + ]); + + const roleByTeam = new Map(myTeamRoles.map((m: any) => [String(m.team), m.role])); + const countByTeam = new Map(memberCounts.map((m: any) => [String(m._id), m.count])); + + return json({ + org: { id: org._id, name: org.name, slug: org.slug, role, memberCount, createdAt: org.createdAt }, + teams: teams.map((t: any) => ({ + id: t._id, + name: t.name, + slug: t.slug, + description: t.description ?? "", + myRole: roleByTeam.get(String(t._id)) ?? null, + access: isOrgAdmin(role) ? "manage" : roleByTeam.get(String(t._id)) === "editor" ? "edit" : "view", + memberCount: countByTeam.get(String(t._id)) ?? 0, + projects: projects + .filter((p: any) => String(p.team) === String(t._id)) + .map((p: any) => ({ id: p._id, name: p.name, slug: p.slug, updatedAt: p.updatedAt, restricted: !!p.restricted })), + })), + }); +}); + +const patchSchema = z.object({ name: nameSchema.optional(), slug: slugSchema.optional() }); + +export const PATCH = route

(async (req: NextRequest, { params }) => { + const me = await requireUser(req); + const { org, role } = await loadOrg(params.org, me); + assertOrgAdmin(role); + const body = await parseBody(req, patchSchema); + + if (body.slug && body.slug !== org.slug) { + if (await Organization.exists({ slug: body.slug })) throw conflict("That URL is already taken.", "slug_taken"); + } + const before = { name: org.name, slug: org.slug }; + Object.assign(org, body); + await org.save(); + await audit(org, me, "org.update", { type: "org", id: String(org._id), name: org.name }, { before, after: body }, req); + + return json({ org: { id: org._id, name: org.name, slug: org.slug, role } }); +}); + +const deleteSchema = z.object({ confirm: z.string() }); + +/** Owner only. Requires typing the org slug; deletes everything in the org. */ +export const DELETE = route

(async (req: NextRequest, { params }) => { + const me = await requireUser(req); + const { org, role } = await loadOrg(params.org, me); + assertOrgOwner(role); + const { confirm } = await parseBody(req, deleteSchema); + if (confirm !== org.slug) throw badRequest(`Type "${org.slug}" to confirm.`, "confirm_mismatch"); + + await Promise.all([ + Environment.deleteMany({ org: org._id }), + Project.deleteMany({ org: org._id }), + TeamMembership.deleteMany({ org: org._id }), + Team.deleteMany({ org: org._id }), + Invite.deleteMany({ org: org._id }), + AuditLog.deleteMany({ org: org._id }), + OrgMembership.deleteMany({ org: org._id }), + ]); + await Organization.deleteOne({ _id: org._id }); + + return json({ message: "Organization deleted." }); +}); diff --git a/web-app/src/app/api/orgs/[org]/search/route.ts b/web-app/src/app/api/orgs/[org]/search/route.ts new file mode 100644 index 0000000..987bcdf --- /dev/null +++ b/web-app/src/app/api/orgs/[org]/search/route.ts @@ -0,0 +1,80 @@ +import { NextRequest } from "next/server"; +import { trusted } from "mongoose"; +import Team from "@/models/team"; +import Project from "@/models/project"; +import Environment from "@/models/environment"; +import { requireUser } from "@/lib/auth"; +import { loadOrg, visibleProjectsFilter, visibleTeamIds } from "@/lib/authz"; +import { json, route } from "@/lib/http"; + +type P = { org: string }; + +const LIMIT = 12; +const escapeRegex = (s: string) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + +/** + * Command menu search across what the caller can see: environments (by name) and variables (by KEY). + * Only names are searched and returned, never values; teams and projects are matched in the browser + * from the navigation tree it already has. + */ +export const GET = route

(async (req: NextRequest, { params }) => { + const me = await requireUser(req); + const { org, role } = await loadOrg(params.org, me); + const q = (req.nextUrl.searchParams.get("q") ?? "").trim().slice(0, 64); + if (q.length < 2) return json({ environments: [], variables: [] }); + + const teamIds = await visibleTeamIds(org._id, role, me.id); + const [teams, projects] = await Promise.all([ + Team.find({ _id: trusted({ $in: teamIds }) }).select("name slug"), + Project.find({ team: trusted({ $in: teamIds }), ...visibleProjectsFilter(role, me.id) }).select("name slug team"), + ]); + if (!projects.length) return json({ environments: [], variables: [] }); + + const teamById = new Map(teams.map((t: any) => [String(t._id), t])); + const projectById = new Map(projects.map((p: any) => [String(p._id), p])); + const projectIds = projects.map((p: any) => p._id); + const pattern = new RegExp(escapeRegex(q), "i"); + + const [envs, vars] = await Promise.all([ + Environment.find({ project: trusted({ $in: projectIds }), name: trusted({ $regex: pattern }) }) + .select("name slug project") + .limit(LIMIT), + Environment.aggregate([ + { $match: { project: { $in: projectIds }, "vars.key": { $regex: pattern } } }, + { $unwind: "$vars" }, + { $match: { "vars.key": { $regex: pattern } } }, + // Exact and prefix matches first, then the rest alphabetically. + { + $addFields: { + rank: { + $cond: [ + { $eq: [{ $toLower: "$vars.key" }, q.toLowerCase()] }, + 0, + { $cond: [{ $eq: [{ $indexOfCP: [{ $toLower: "$vars.key" }, q.toLowerCase()] }, 0] }, 1, 2] }, + ], + }, + }, + }, + { $sort: { rank: 1, "vars.key": 1, position: 1 } }, + { $limit: LIMIT * 2 }, + { $project: { _id: 0, key: "$vars.key", project: 1, name: 1, slug: 1 } }, + ]), + ]); + + const place = (projectId: unknown) => { + const p: any = projectById.get(String(projectId)); + const t: any = p && teamById.get(String(p.team)); + return p && t ? { project: { name: p.name, slug: p.slug }, team: { name: t.name, slug: t.slug } } : null; + }; + + return json({ + environments: envs.flatMap((e: any) => { + const at = place(e.project); + return at ? [{ name: e.name, slug: e.slug, ...at }] : []; + }), + variables: vars.flatMap((v: any) => { + const at = place(v.project); + return at ? [{ key: v.key, env: { name: v.name, slug: v.slug }, ...at }] : []; + }).slice(0, LIMIT), + }); +}); diff --git a/web-app/src/app/api/orgs/[org]/security/route.ts b/web-app/src/app/api/orgs/[org]/security/route.ts new file mode 100644 index 0000000..649e686 --- /dev/null +++ b/web-app/src/app/api/orgs/[org]/security/route.ts @@ -0,0 +1,55 @@ +import { NextRequest } from "next/server"; +import { z } from "zod"; +import OrgMembership from "@/models/orgMembership"; +import User from "@/models/user"; +import { requireUser } from "@/lib/auth"; +import { audit } from "@/lib/audit"; +import { assertOrgOwner, isOrgAdmin, loadOrg, requires2fa } from "@/lib/authz"; +import { badRequest, json, parseBody, route } from "@/lib/http"; + +type P = { org: string }; + +/** Members who have neither a passkey nor an authenticator app. */ +async function membersWithout2fa(orgId: unknown) { + const rows = await OrgMembership.aggregate([ + { $match: { org: orgId } }, + { $lookup: { from: User.collection.name, localField: "user", foreignField: "_id", as: "u" } }, + { $unwind: "$u" }, + { $match: { "u.totpEnabled": { $ne: true }, $or: [{ "u.passkeyCount": { $exists: false } }, { "u.passkeyCount": { $lte: 0 } }] } }, + { $sort: { "u.name": 1 } }, + { $project: { _id: 0, name: "$u.name", email: "$u.email", image: "$u.image" } }, + ]); + return rows as { name: string; email: string; image?: string | null }[]; +} + +/** The org's two-factor requirement, and (for owners and admins) who hasn't set 2FA up yet. */ +export const GET = route

(async (req: NextRequest, { params }) => { + const me = await requireUser(req); + const { org, role } = await loadOrg(params.org, me); + const without = isOrgAdmin(role) ? await membersWithout2fa(org._id) : null; + return json({ require2fa: requires2fa(org), membersWithout2fa: without }); +}); + +const schema = z.object({ require2fa: z.boolean() }); + +/** + * Owner only. Turning it on locks members without 2FA out of the org until they add a passkey or an + * authenticator app; turning it off never removes anyone's passkeys or apps. + */ +export const PATCH = route

(async (req: NextRequest, { params }) => { + const me = await requireUser(req); + const { org, role } = await loadOrg(params.org, me); + assertOrgOwner(role); + const { require2fa } = await parseBody(req, schema); + if (require2fa && !me.mfa) { + throw badRequest("Set up a passkey or an authenticator app for yourself first, so you aren't locked out.", "owner_needs_2fa"); + } + + const before = requires2fa(org); + org.require2fa = require2fa; + await org.save(); + if (before !== require2fa) { + await audit(org, me, require2fa ? "org.2fa_required" : "org.2fa_optional", { type: "org", id: String(org._id), name: org.name }, undefined, req); + } + return json({ require2fa, membersWithout2fa: await membersWithout2fa(org._id) }); +}); diff --git a/web-app/src/app/api/orgs/[org]/teams/[team]/members/[userId]/route.ts b/web-app/src/app/api/orgs/[org]/teams/[team]/members/[userId]/route.ts new file mode 100644 index 0000000..103a179 --- /dev/null +++ b/web-app/src/app/api/orgs/[org]/teams/[team]/members/[userId]/route.ts @@ -0,0 +1,48 @@ +import { NextRequest } from "next/server"; +import { z } from "zod"; +import TeamMembership from "@/models/teamMembership"; +import Project from "@/models/project"; +import { requireUser } from "@/lib/auth"; +import { audit } from "@/lib/audit"; +import { assertOrgAdmin, loadOrg, loadTeam } from "@/lib/authz"; +import { json, notFound, parseBody, route } from "@/lib/http"; +import { teamRoleSchema } from "@/lib/validation"; + +type P = { org: string; team: string; userId: string }; + +const schema = z.object({ role: teamRoleSchema }); + +export const PATCH = route

(async (req: NextRequest, { params }) => { + const me = await requireUser(req); + const ctx = await loadTeam(await loadOrg(params.org, me), params.team, me); + assertOrgAdmin(ctx.role); + const { role } = await parseBody(req, schema); + + const tm = await TeamMembership.findOne({ team: ctx.team._id, user: params.userId }).populate("user", "email"); + if (!tm) throw notFound("Member not found."); + const from = tm.role; + tm.role = role; + await tm.save(); + await audit(ctx.org, me, "team.member_role_change", { type: "team", id: String(ctx.team._id), name: ctx.team.name }, { user: tm.user?.email, from, to: role }, req); + + return json({ message: "Role updated." }); +}); + +export const DELETE = route

(async (req: NextRequest, { params }) => { + const me = await requireUser(req); + const ctx = await loadTeam(await loadOrg(params.org, me), params.team, me); + assertOrgAdmin(ctx.role); + + const tm = await TeamMembership.findOne({ team: ctx.team._id, user: params.userId }).populate("user", "email"); + if (!tm) throw notFound("Member not found."); + await TeamMembership.deleteOne({ _id: tm._id }); + // Drop them from the team's "Only selected people" lists, so being added back later doesn't restore access. + await Project.updateMany( + { team: ctx.team._id, allowedUsers: params.userId }, + { $pull: { allowedUsers: params.userId } }, + { timestamps: false } + ); + await audit(ctx.org, me, "team.member_remove", { type: "team", id: String(ctx.team._id), name: ctx.team.name }, { user: tm.user?.email }, req); + + return json({ message: "Removed from team." }); +}); diff --git a/web-app/src/app/api/orgs/[org]/teams/[team]/members/route.ts b/web-app/src/app/api/orgs/[org]/teams/[team]/members/route.ts new file mode 100644 index 0000000..de6cf26 --- /dev/null +++ b/web-app/src/app/api/orgs/[org]/teams/[team]/members/route.ts @@ -0,0 +1,57 @@ +import { NextRequest } from "next/server"; +import { z } from "zod"; +import OrgMembership from "@/models/orgMembership"; +import TeamMembership from "@/models/teamMembership"; +import { requireUser } from "@/lib/auth"; +import { audit } from "@/lib/audit"; +import { assertOrgAdmin, loadOrg, loadTeam } from "@/lib/authz"; +import { badRequest, conflict, json, parseBody, route } from "@/lib/http"; +import { teamRoleSchema } from "@/lib/validation"; +import { afterFilter, readPage, sortSpec, toPage, type SortField } from "@/lib/pagination"; + +const MEMBER_SORT: SortField[] = [{ key: "_id", dir: 1, type: "id" }]; + +/** People on the team, in the order they were added. Paginated; `total` counts them all. */ +export const GET = route<{ org: string; team: string }>(async (req: NextRequest, { params }) => { + const me = await requireUser(req); + const ctx = await loadTeam(await loadOrg(params.org, me), params.team, me); + const { limit, after } = readPage(req, MEMBER_SORT); + + const [rows, total] = await Promise.all([ + TeamMembership.find({ team: ctx.team._id, ...afterFilter(MEMBER_SORT, after) }) + .populate("user", "name email image") + .sort(sortSpec(MEMBER_SORT)) + .limit(limit + 1), + TeamMembership.countDocuments({ team: ctx.team._id }), + ]); + const { items, nextCursor } = toPage(rows as any[], limit, (m) => [m._id]); + + return json({ + total, + nextCursor, + members: items + .filter((m) => m.user) + .map((m) => ({ userId: m.user._id, name: m.user.name, email: m.user.email, image: m.user.image ?? null, role: m.role })), + }); +}); + +const schema = z.object({ userId: z.string().min(1), role: teamRoleSchema }); + +/** Add an existing org member to the team. People outside the org are invited instead. */ +export const POST = route<{ org: string; team: string }>(async (req: NextRequest, { params }) => { + const me = await requireUser(req); + const ctx = await loadTeam(await loadOrg(params.org, me), params.team, me); + assertOrgAdmin(ctx.role); + const { userId, role } = await parseBody(req, schema); + + const orgMember = await OrgMembership.findOne({ org: ctx.org._id, user: userId }).populate("user", "name email"); + if (!orgMember) throw badRequest("Invite this person to the organization first.", "not_org_member"); + if (await TeamMembership.exists({ team: ctx.team._id, user: userId })) { + throw conflict("Already a member of this team.", "already_member"); + } + + await TeamMembership.create({ team: ctx.team._id, org: ctx.org._id, user: userId, role }); + await audit(ctx.org, me, "team.member_add", { type: "team", id: String(ctx.team._id), name: ctx.team.name }, { user: orgMember.user?.email, role }, req); + + return json({ message: "Added to team." }, 201); +}); diff --git a/web-app/src/app/api/orgs/[org]/teams/[team]/projects/[project]/access/route.ts b/web-app/src/app/api/orgs/[org]/teams/[team]/projects/[project]/access/route.ts new file mode 100644 index 0000000..aac78e7 --- /dev/null +++ b/web-app/src/app/api/orgs/[org]/teams/[team]/projects/[project]/access/route.ts @@ -0,0 +1,122 @@ +import { NextRequest } from "next/server"; +import { z } from "zod"; +import { trusted } from "mongoose"; +import Project from "@/models/project"; +import OrgMembership from "@/models/orgMembership"; +import TeamMembership from "@/models/teamMembership"; +import { requireUser } from "@/lib/auth"; +import { audit } from "@/lib/audit"; +import { assertOrgAdmin, loadOrg, loadProject, loadTeam } from "@/lib/authz"; +import { badRequest, json, parseBody, route } from "@/lib/http"; + +type P = { org: string; team: string; project: string }; + +/** Owners and admins only: who can open the project. */ +async function load(req: NextRequest, params: P) { + const me = await requireUser(req); + const ctx = await loadProject(await loadTeam(await loadOrg(params.org, me), params.team, me), params.project); + assertOrgAdmin(ctx.role); + return { me, ctx }; +} + +/** + * The project's access setting and every team member, each with whether they're allowed in. + * Members who are org owners or admins are `alwaysAllowed`: they see every project. + */ +export const GET = route

(async (req: NextRequest, { params }) => { + const { ctx } = await load(req, params); + + const rows = await TeamMembership.find({ team: ctx.team._id }).populate("user", "name email image").sort({ _id: 1 }); + const members = (rows as any[]).filter((m) => m.user); + const admins = await OrgMembership.find({ + org: ctx.org._id, + user: trusted({ $in: members.map((m) => m.user._id) }), + role: trusted({ $in: ["owner", "admin"] }), + }).select("user"); + const adminIds = new Set(admins.map((a: any) => String(a.user))); + const allowed = new Set((ctx.project.allowedUsers ?? []).map((u: unknown) => String(u))); + + return json({ + restricted: !!ctx.project.restricted, + members: members.map((m) => ({ + userId: m.user._id, + name: m.user.name, + email: m.user.email, + image: m.user.image ?? null, + role: m.role, + allowed: allowed.has(String(m.user._id)), + alwaysAllowed: adminIds.has(String(m.user._id)), + })), + }); +}); + +const putSchema = z.object({ + restricted: z.boolean(), + userIds: z.array(z.string().regex(/^[a-f0-9]{24}$/i, "Invalid user id.")).max(1000).default([]), +}); + +/** + * Set who can open the project: everyone on the team (`restricted: false`), or only the team + * members in `userIds`. Owners and admins keep access either way. + */ +export const PUT = route

(async (req: NextRequest, { params }) => { + const { me, ctx } = await load(req, params); + const body = await parseBody(req, putSchema); + + // "Everyone on the team" keeps no list, so switching back starts from a clean selection. + const ids = body.restricted ? [...new Set(body.userIds.map((id) => id.toLowerCase()))] : []; + const memberships = ids.length + ? await TeamMembership.find({ team: ctx.team._id, user: trusted({ $in: ids }) }).populate("user", "name email") + : []; + const found = new Set(memberships.map((m: any) => String(m.user?._id ?? m.user))); + const outsiders = ids.filter((id) => !found.has(id)); + if (outsiders.length) { + throw badRequest( + outsiders.length === 1 + ? `One of the selected people isn't on ${ctx.team.name}. Only team members can be given access.` + : `${outsiders.length} of the selected people aren't on ${ctx.team.name}. Only team members can be given access.`, + "not_team_member" + ); + } + + const wasRestricted = !!ctx.project.restricted; + const before = new Set((ctx.project.allowedUsers ?? []).map((u: unknown) => String(u))); + const nameOf = new Map(memberships.map((m: any) => [String(m.user._id), m.user.name ?? m.user.email])); + + // Changing who can see it isn't an edit to the project's contents, so `updatedAt` stays. + await Project.updateOne( + { _id: ctx.project._id }, + { $set: { restricted: body.restricted, allowedUsers: ids } }, + { timestamps: false } + ); + + const addedIds = ids.filter((id) => !before.has(id)); + const removedIds = [...before].filter((id) => !ids.includes(id)); + if (wasRestricted !== body.restricted || addedIds.length || removedIds.length) { + // Names for people who lost access may need a lookup: they're not in this request's list. + const removedRows = removedIds.length + ? await TeamMembership.find({ team: ctx.team._id, user: trusted({ $in: removedIds }) }).populate("user", "name email") + : []; + const removedNames = new Map(removedRows.map((m: any) => [String(m.user?._id), m.user?.name ?? m.user?.email])); + await audit( + ctx.org, + me, + "project.access_change", + { type: "project", id: String(ctx.project._id), name: `${ctx.team.name}/${ctx.project.name}` }, + { + from: wasRestricted ? "selected" : "everyone", + to: body.restricted ? "selected" : "everyone", + allowed: ids.map((id) => nameOf.get(id)), + added: addedIds.map((id) => nameOf.get(id)), + removed: removedIds.map((id) => removedNames.get(id) ?? "Former member"), + }, + req + ); + } + + return json({ + message: body.restricted ? "Only selected people can open this project." : "Everyone on the team can open this project.", + restricted: body.restricted, + allowedCount: ids.length, + }); +}); diff --git a/web-app/src/app/api/orgs/[org]/teams/[team]/projects/[project]/envs/[env]/route.ts b/web-app/src/app/api/orgs/[org]/teams/[team]/projects/[project]/envs/[env]/route.ts new file mode 100644 index 0000000..f95a38b --- /dev/null +++ b/web-app/src/app/api/orgs/[org]/teams/[team]/projects/[project]/envs/[env]/route.ts @@ -0,0 +1,75 @@ +import { NextRequest } from "next/server"; +import { z } from "zod"; +import Environment from "@/models/environment"; +import { requireUser } from "@/lib/auth"; +import { audit } from "@/lib/audit"; +import { assertCanEdit, assertOrgAdmin, loadEnvironment, loadOrg, loadProject, loadTeam } from "@/lib/authz"; +import { badRequest, json, parseBody, route } from "@/lib/http"; +import { decrypt } from "@/utils/encryption"; +import User from "@/models/user"; +import { revealRequired, revealUntil } from "@/lib/reveal"; + +type P = { org: string; team: string; project: string; env: string }; + +async function load(req: NextRequest, params: P) { + const me = await requireUser(req); + const ctx = await loadEnvironment( + await loadProject(await loadTeam(await loadOrg(params.org, me), params.team, me), params.project), + params.env + ); + return { me, ctx }; +} + +/** + * One environment's variables. Values are only included while the caller has unlocked them with a fresh + * authenticator code (see lib/reveal); otherwise each value is null and only key names are sent. + */ +export const GET = route

(async (req: NextRequest, { params }) => { + const { me, ctx } = await load(req, params); + const until = await revealUntil(me); + const required = revealRequired(me); + const show = !required || !!until; + const env = ctx.environment; + const updatedBy = env.updatedBy ? await User.findById(env.updatedBy).select("name") : null; + + return json({ + environment: { + id: env._id, + name: env.name, + slug: env.slug, + version: env.version ?? 0, + updatedAt: env.updatedAt, + updatedBy: updatedBy?.name ?? null, + vars: (env.vars ?? []).map((v: any) => ({ key: v.key, value: show ? decrypt(v.value) : null })), + }, + access: ctx.access, + revealUntil: until, + /** False for people without 2FA: their values come unlocked, with no timer. */ + revealRequired: required, + }); +}); + +const patchSchema = z.object({ name: z.string().trim().min(1).max(32) }); + +export const PATCH = route

(async (req: NextRequest, { params }) => { + const { me, ctx } = await load(req, params); + assertCanEdit(ctx.access); + const { name } = await parseBody(req, patchSchema); + const from = ctx.environment.name; + ctx.environment.name = name; + await ctx.environment.save(); + await audit(ctx.org, me, "env.update", { type: "environment", id: String(ctx.environment._id), name: `${ctx.team.name}/${ctx.project.name}/${name}` }, { from, to: name }, req); + return json({ message: "Environment renamed." }); +}); + +/** Owner/admin only; the last environment of a project can't be deleted. */ +export const DELETE = route

(async (req: NextRequest, { params }) => { + const { me, ctx } = await load(req, params); + assertOrgAdmin(ctx.role); + if ((await Environment.countDocuments({ project: ctx.project._id })) <= 1) { + throw badRequest("A project needs at least one environment."); + } + await Environment.deleteOne({ _id: ctx.environment._id }); + await audit(ctx.org, me, "env.delete", { type: "environment", id: String(ctx.environment._id), name: `${ctx.team.name}/${ctx.project.name}/${ctx.environment.name}` }, { vars: ctx.environment.vars?.length ?? 0 }, req); + return json({ message: "Environment deleted." }); +}); diff --git a/web-app/src/app/api/orgs/[org]/teams/[team]/projects/[project]/envs/[env]/vars/route.ts b/web-app/src/app/api/orgs/[org]/teams/[team]/projects/[project]/envs/[env]/vars/route.ts new file mode 100644 index 0000000..99e1000 --- /dev/null +++ b/web-app/src/app/api/orgs/[org]/teams/[team]/projects/[project]/envs/[env]/vars/route.ts @@ -0,0 +1,77 @@ +import { NextRequest } from "next/server"; +import { z } from "zod"; +import Environment from "@/models/environment"; +import User from "@/models/user"; +import { requireUser } from "@/lib/auth"; +import { audit } from "@/lib/audit"; +import { assertCanEdit, loadEnvironment, loadOrg, loadProject, loadTeam } from "@/lib/authz"; +import { badRequest, conflict, json, parseBody, route } from "@/lib/http"; +import { envVarSaveSchema } from "@/lib/validation"; +import { decrypt, encrypt } from "@/utils/encryption"; + +type P = { org: string; team: string; project: string; env: string }; + +const schema = z.object({ vars: envVarSaveSchema, version: z.number().int().min(0) }); + +/** + * Replaces the environment's variables. `version` must match the stored version + * (optimistic concurrency) so two editors can't silently overwrite each other. + * The audit log records key names that were added/changed/removed, never values. + * + * A row may omit `value` to keep the stored one, so editors can add, rename and remove keys without + * unlocking values. `from` names the key the kept value comes from (for a rename); it defaults to `key`. + */ +export const PUT = route

(async (req: NextRequest, { params }) => { + const me = await requireUser(req); + const ctx = await loadEnvironment( + await loadProject(await loadTeam(await loadOrg(params.org, me), params.team, me), params.project), + params.env + ); + assertCanEdit(ctx.access); + const { vars, version } = await parseBody(req, schema); + + const previous = new Map( + (ctx.environment.vars ?? []).map((v: any) => [v.key, decrypt(v.value)]) + ); + const resolved = vars.map((v) => { + if (v.value !== undefined) return { key: v.key, value: v.value }; + const source = v.from ?? v.key; + if (!previous.has(source)) throw badRequest(`"${source}" has no stored value to keep. Enter a value for "${v.key}".`, "missing_value"); + return { key: v.key, value: previous.get(source)! }; + }); + const next = new Map(resolved.map((v) => [v.key, v.value])); + const added = [...next.keys()].filter((k) => !previous.has(k)); + const removed = [...previous.keys()].filter((k) => !next.has(k)); + const changed = [...next.keys()].filter((k) => previous.has(k) && previous.get(k) !== next.get(k)); + + const updated = await Environment.findOneAndUpdate( + { _id: ctx.environment._id, version }, + { + $set: { vars: resolved.map(({ key, value }) => ({ key, value: encrypt(value) })), updatedBy: me.id }, + $inc: { version: 1 }, + }, + { new: true } + ); + + if (!updated) { + const current = await Environment.findById(ctx.environment._id).select("updatedBy updatedAt"); + const by = current?.updatedBy ? await User.findById(current.updatedBy).select("name") : null; + throw conflict( + `These variables were changed${by ? ` by ${by.name}` : ""} since you loaded them. Reload to see the latest version.`, + "version_conflict" + ); + } + + if (added.length || removed.length || changed.length) { + await audit( + ctx.org, + me, + "vars.update", + { type: "environment", id: String(ctx.environment._id), name: `${ctx.team.name}/${ctx.project.name}/${ctx.environment.name}` }, + { added, removed, changed }, + req + ); + } + + return json({ version: updated.version, updatedAt: updated.updatedAt, added, removed, changed }); +}); diff --git a/web-app/src/app/api/orgs/[org]/teams/[team]/projects/[project]/envs/route.ts b/web-app/src/app/api/orgs/[org]/teams/[team]/projects/[project]/envs/route.ts new file mode 100644 index 0000000..530b414 --- /dev/null +++ b/web-app/src/app/api/orgs/[org]/teams/[team]/projects/[project]/envs/route.ts @@ -0,0 +1,25 @@ +import { NextRequest } from "next/server"; +import { z } from "zod"; +import Environment from "@/models/environment"; +import { requireUser } from "@/lib/auth"; +import { audit } from "@/lib/audit"; +import { assertCanEdit, loadOrg, loadProject, loadTeam } from "@/lib/authz"; +import { badRequest, json, parseBody, route } from "@/lib/http"; +import { uniqueSlug } from "@/lib/validation"; + +const schema = z.object({ name: z.string().trim().min(1).max(32) }); + +export const POST = route<{ org: string; team: string; project: string }>(async (req: NextRequest, { params }) => { + const me = await requireUser(req); + const ctx = await loadProject(await loadTeam(await loadOrg(params.org, me), params.team, me), params.project); + assertCanEdit(ctx.access); + const { name } = await parseBody(req, schema); + + const count = await Environment.countDocuments({ project: ctx.project._id }); + if (count >= 20) throw badRequest("A project can have at most 20 environments."); + const slug = await uniqueSlug(name, (s) => Environment.exists({ project: ctx.project._id, slug: s })); + const env = await Environment.create({ project: ctx.project._id, org: ctx.org._id, name, slug, position: count, vars: [] }); + await audit(ctx.org, me, "env.create", { type: "environment", id: String(env._id), name: `${ctx.team.name}/${ctx.project.name}/${name}` }, undefined, req); + + return json({ environment: { id: env._id, name: env.name, slug: env.slug } }, 201); +}); diff --git a/web-app/src/app/api/orgs/[org]/teams/[team]/projects/[project]/move/route.ts b/web-app/src/app/api/orgs/[org]/teams/[team]/projects/[project]/move/route.ts new file mode 100644 index 0000000..16790a1 --- /dev/null +++ b/web-app/src/app/api/orgs/[org]/teams/[team]/projects/[project]/move/route.ts @@ -0,0 +1,56 @@ +import { NextRequest } from "next/server"; +import { z } from "zod"; +import { trusted } from "mongoose"; +import Project from "@/models/project"; +import Team from "@/models/team"; +import TeamMembership from "@/models/teamMembership"; +import { requireUser } from "@/lib/auth"; +import { audit } from "@/lib/audit"; +import { assertOrgAdmin, loadOrg, loadProject, loadTeam } from "@/lib/authz"; +import { badRequest, conflict, json, parseBody, route } from "@/lib/http"; +import { slugSchema } from "@/lib/validation"; + +type P = { org: string; team: string; project: string }; + +const schema = z.object({ teamSlug: slugSchema }); + +/** + * Owners and admins: moves a project, with its environments and variables, to another team in the org. + * Access then follows the new team. For a project limited to selected people, only those who are also on + * the new team keep access. + */ +export const POST = route

(async (req: NextRequest, { params }) => { + const me = await requireUser(req); + const ctx = await loadProject(await loadTeam(await loadOrg(params.org, me), params.team, me), params.project); + assertOrgAdmin(ctx.role); + const { teamSlug } = await parseBody(req, schema); + + const target = await Team.findOne({ org: ctx.org._id, slug: teamSlug.toLowerCase() }); + if (!target) throw badRequest("That team doesn't exist in this organization.", "team_not_found"); + if (String(target._id) === String(ctx.team._id)) throw badRequest("The project is already in that team.", "same_team"); + if (await Project.exists({ team: target._id, slug: ctx.project.slug })) { + throw conflict(`${target.name} already has a project at /${ctx.project.slug}. Rename one of them first.`, "slug_taken"); + } + + const project = ctx.project; + let dropped = 0; + if (project.restricted && project.allowedUsers?.length) { + const stay = await TeamMembership.find({ team: target._id, user: trusted({ $in: project.allowedUsers }) }).select("user"); + const keep = stay.map((m: any) => m.user); + dropped = project.allowedUsers.length - keep.length; + project.allowedUsers = keep; + } + project.team = target._id; + await project.save(); + + await audit( + ctx.org, + me, + "project.move", + { type: "project", id: String(project._id), name: project.name }, + { from: ctx.team.name, to: target.name, ...(dropped ? { accessRemoved: dropped } : {}) }, + req + ); + + return json({ project: { slug: project.slug }, team: { slug: target.slug, name: target.name } }); +}); diff --git a/web-app/src/app/api/orgs/[org]/teams/[team]/projects/[project]/route.ts b/web-app/src/app/api/orgs/[org]/teams/[team]/projects/[project]/route.ts new file mode 100644 index 0000000..eb640fc --- /dev/null +++ b/web-app/src/app/api/orgs/[org]/teams/[team]/projects/[project]/route.ts @@ -0,0 +1,71 @@ +import { NextRequest } from "next/server"; +import { z } from "zod"; +import Project from "@/models/project"; +import Environment from "@/models/environment"; +import { requireUser } from "@/lib/auth"; +import { audit } from "@/lib/audit"; +import { assertCanEdit, assertOrgAdmin, loadOrg, loadProject, loadTeam } from "@/lib/authz"; +import { badRequest, json, parseBody, route } from "@/lib/http"; +import { nameSchema } from "@/lib/validation"; + +type P = { org: string; team: string; project: string }; + +async function load(req: NextRequest, params: P) { + const me = await requireUser(req); + const ctx = await loadProject(await loadTeam(await loadOrg(params.org, me), params.team, me), params.project); + return { me, ctx }; +} + +export const GET = route

(async (req: NextRequest, { params }) => { + const { ctx } = await load(req, params); + const envs = await Environment.find({ project: ctx.project._id }) + .select("name slug position version updatedAt vars.key") + .sort({ position: 1, createdAt: 1 }); + + return json({ + project: { + id: ctx.project._id, + name: ctx.project.name, + slug: ctx.project.slug, + description: ctx.project.description ?? "", + restricted: !!ctx.project.restricted, + }, + team: { name: ctx.team.name, slug: ctx.team.slug }, + org: { name: ctx.org.name, slug: ctx.org.slug }, + access: ctx.access, + environments: envs.map((e: any) => ({ + id: e._id, + name: e.name, + slug: e.slug, + varCount: e.vars?.length ?? 0, + updatedAt: e.updatedAt, + })), + }); +}); + +const patchSchema = z.object({ name: nameSchema.optional(), description: z.string().trim().max(280).optional() }); + +export const PATCH = route

(async (req: NextRequest, { params }) => { + const { me, ctx } = await load(req, params); + assertCanEdit(ctx.access); + const body = await parseBody(req, patchSchema); + Object.assign(ctx.project, body); + await ctx.project.save(); + await audit(ctx.org, me, "project.update", { type: "project", id: String(ctx.project._id), name: `${ctx.team.name}/${ctx.project.name}` }, body, req); + return json({ message: "Project updated." }); +}); + +const deleteSchema = z.object({ confirm: z.string() }); + +/** Org owner/admin only; requires typing the project slug. */ +export const DELETE = route

(async (req: NextRequest, { params }) => { + const { me, ctx } = await load(req, params); + assertOrgAdmin(ctx.role); + const { confirm } = await parseBody(req, deleteSchema); + if (confirm !== ctx.project.slug) throw badRequest(`Type "${ctx.project.slug}" to confirm.`, "confirm_mismatch"); + + await Environment.deleteMany({ project: ctx.project._id }); + await Project.deleteOne({ _id: ctx.project._id }); + await audit(ctx.org, me, "project.delete", { type: "project", id: String(ctx.project._id), name: `${ctx.team.name}/${ctx.project.name}` }, undefined, req); + return json({ message: "Project deleted." }); +}); diff --git a/web-app/src/app/api/orgs/[org]/teams/[team]/projects/route.ts b/web-app/src/app/api/orgs/[org]/teams/[team]/projects/route.ts new file mode 100644 index 0000000..da6340d --- /dev/null +++ b/web-app/src/app/api/orgs/[org]/teams/[team]/projects/route.ts @@ -0,0 +1,77 @@ +import { NextRequest } from "next/server"; +import { z } from "zod"; +import Project from "@/models/project"; +import Environment, { DEFAULT_ENVIRONMENTS } from "@/models/environment"; +import { requireUser } from "@/lib/auth"; +import { audit } from "@/lib/audit"; +import { assertCanEdit, loadOrg, loadTeam, visibleProjectsFilter } from "@/lib/authz"; +import { json, parseBody, route } from "@/lib/http"; +import { nameSchema, uniqueSlug } from "@/lib/validation"; +import { afterFilter, readPage, sortSpec, toPage, type SortField } from "@/lib/pagination"; + +const PROJECT_SORT: SortField[] = [ + { key: "name", dir: 1, type: "string" }, + { key: "_id", dir: 1, type: "id" }, +]; + +/** + * The team's projects the caller can see, by name, with environment and variable counts. + * Paginated; `total` counts every visible one. + */ +export const GET = route<{ org: string; team: string }>(async (req: NextRequest, { params }) => { + const me = await requireUser(req); + const ctx = await loadTeam(await loadOrg(params.org, me), params.team, me); + const { limit, after } = readPage(req, PROJECT_SORT); + const visible = visibleProjectsFilter(ctx.role, ctx.userId); + + const [rows, total] = await Promise.all([ + Project.find({ team: ctx.team._id, ...visible, ...afterFilter(PROJECT_SORT, after) }) + .sort(sortSpec(PROJECT_SORT)) + .limit(limit + 1), + Project.countDocuments({ team: ctx.team._id, ...visible }), + ]); + const { items, nextCursor } = toPage(rows as any[], limit, (p) => [p.name, p._id]); + + const counts = new Map( + ( + await Environment.aggregate([ + { $match: { project: { $in: items.map((p) => p._id) } } }, + { $group: { _id: "$project", envs: { $sum: 1 }, vars: { $sum: { $size: "$vars" } } } }, + ]) + ).map((c: any) => [String(c._id), c]) + ); + + return json({ + total, + nextCursor, + projects: items.map((p) => ({ + id: p._id, + name: p.name, + slug: p.slug, + description: p.description ?? "", + restricted: !!p.restricted, + updatedAt: p.updatedAt, + envCount: counts.get(String(p._id))?.envs ?? 0, + varCount: counts.get(String(p._id))?.vars ?? 0, + })), + }); +}); + +const schema = z.object({ name: nameSchema, description: z.string().trim().max(280).optional() }); + +/** Editors and admins can create projects; each starts with development/staging/production. */ +export const POST = route<{ org: string; team: string }>(async (req: NextRequest, { params }) => { + const me = await requireUser(req); + const ctx = await loadTeam(await loadOrg(params.org, me), params.team, me); + assertCanEdit(ctx.access); + const { name, description } = await parseBody(req, schema); + + const slug = await uniqueSlug(name, (s) => Project.exists({ team: ctx.team._id, slug: s })); + const project = await Project.create({ org: ctx.org._id, team: ctx.team._id, name, slug, description }); + await Environment.insertMany( + DEFAULT_ENVIRONMENTS.map((e, i) => ({ ...e, position: i, project: project._id, org: ctx.org._id, vars: [] })) + ); + await audit(ctx.org, me, "project.create", { type: "project", id: String(project._id), name: `${ctx.team.name}/${name}` }, undefined, req); + + return json({ project: { id: project._id, name: project.name, slug: project.slug } }, 201); +}); diff --git a/web-app/src/app/api/orgs/[org]/teams/[team]/route.ts b/web-app/src/app/api/orgs/[org]/teams/[team]/route.ts new file mode 100644 index 0000000..41cb2f2 --- /dev/null +++ b/web-app/src/app/api/orgs/[org]/teams/[team]/route.ts @@ -0,0 +1,76 @@ +import { NextRequest } from "next/server"; +import { z } from "zod"; +import Team from "@/models/team"; +import TeamMembership from "@/models/teamMembership"; +import Project from "@/models/project"; +import Environment from "@/models/environment"; +import { requireUser } from "@/lib/auth"; +import { audit } from "@/lib/audit"; +import { assertOrgAdmin, loadOrg, loadTeam, visibleProjectsFilter } from "@/lib/authz"; +import { badRequest, json, parseBody, route } from "@/lib/http"; +import { nameSchema } from "@/lib/validation"; +import { trusted } from "mongoose"; + +type P = { org: string; team: string }; + +/** Team details and counts. Its projects and members are paginated at /projects and /members. */ +export const GET = route

(async (req: NextRequest, { params }) => { + const me = await requireUser(req); + const ctx = await loadTeam(await loadOrg(params.org, me), params.team, me); + + const [projectCount, memberCount] = await Promise.all([ + Project.countDocuments({ team: ctx.team._id, ...visibleProjectsFilter(ctx.role, ctx.userId) }), + TeamMembership.countDocuments({ team: ctx.team._id }), + ]); + + return json({ + team: { + id: ctx.team._id, + name: ctx.team.name, + slug: ctx.team.slug, + description: ctx.team.description ?? "", + access: ctx.access, + myRole: ctx.teamRole, + }, + orgRole: ctx.role, + projectCount, + memberCount, + }); +}); + +const patchSchema = z.object({ name: nameSchema.optional(), description: z.string().trim().max(280).optional() }); + +export const PATCH = route

(async (req: NextRequest, { params }) => { + const me = await requireUser(req); + const ctx = await loadTeam(await loadOrg(params.org, me), params.team, me); + assertOrgAdmin(ctx.role); + const body = await parseBody(req, patchSchema); + + Object.assign(ctx.team, body); + await ctx.team.save(); + await audit(ctx.org, me, "team.update", { type: "team", id: String(ctx.team._id), name: ctx.team.name }, body, req); + return json({ team: { id: ctx.team._id, name: ctx.team.name, slug: ctx.team.slug } }); +}); + +const deleteSchema = z.object({ confirm: z.string().optional() }); + +/** Deleting a team with projects requires typing its slug; cascades projects and environments. */ +export const DELETE = route

(async (req: NextRequest, { params }) => { + const me = await requireUser(req); + const ctx = await loadTeam(await loadOrg(params.org, me), params.team, me); + assertOrgAdmin(ctx.role); + const { confirm } = await parseBody(req, deleteSchema); + + const projectIds = (await Project.find({ team: ctx.team._id }).select("_id")).map((p: any) => p._id); + if (projectIds.length && confirm !== ctx.team.slug) { + throw badRequest(`This team has ${projectIds.length} project(s). Type "${ctx.team.slug}" to confirm.`, "confirm_mismatch"); + } + + await Environment.deleteMany({ project: trusted({ $in: projectIds }) }); + await Project.deleteMany({ team: ctx.team._id }); + await TeamMembership.deleteMany({ team: ctx.team._id }); + await Team.deleteOne({ _id: ctx.team._id }); + await audit(ctx.org, me, "team.delete", { type: "team", id: String(ctx.team._id), name: ctx.team.name }, { projects: projectIds.length }, req); + + return json({ message: "Team deleted." }); +}); diff --git a/web-app/src/app/api/orgs/[org]/teams/route.ts b/web-app/src/app/api/orgs/[org]/teams/route.ts new file mode 100644 index 0000000..695380c --- /dev/null +++ b/web-app/src/app/api/orgs/[org]/teams/route.ts @@ -0,0 +1,23 @@ +import { NextRequest } from "next/server"; +import { z } from "zod"; +import Team from "@/models/team"; +import { requireUser } from "@/lib/auth"; +import { audit } from "@/lib/audit"; +import { assertOrgAdmin, loadOrg } from "@/lib/authz"; +import { json, parseBody, route } from "@/lib/http"; +import { nameSchema, uniqueSlug } from "@/lib/validation"; + +const schema = z.object({ name: nameSchema, description: z.string().trim().max(280).optional() }); + +export const POST = route<{ org: string }>(async (req: NextRequest, { params }) => { + const me = await requireUser(req); + const { org, role } = await loadOrg(params.org, me); + assertOrgAdmin(role); + const { name, description } = await parseBody(req, schema); + + const slug = await uniqueSlug(name, (s) => Team.exists({ org: org._id, slug: s })); + const team = await Team.create({ org: org._id, name, slug, description }); + await audit(org, me, "team.create", { type: "team", id: String(team._id), name }, undefined, req); + + return json({ team: { id: team._id, name: team.name, slug: team.slug } }, 201); +}); diff --git a/web-app/src/app/api/orgs/[org]/transfer/route.ts b/web-app/src/app/api/orgs/[org]/transfer/route.ts new file mode 100644 index 0000000..3f4e6e9 --- /dev/null +++ b/web-app/src/app/api/orgs/[org]/transfer/route.ts @@ -0,0 +1,30 @@ +import { NextRequest } from "next/server"; +import { z } from "zod"; +import OrgMembership from "@/models/orgMembership"; +import { requireUser } from "@/lib/auth"; +import { audit } from "@/lib/audit"; +import { assertOrgOwner, loadOrg } from "@/lib/authz"; +import { badRequest, json, parseBody, route } from "@/lib/http"; + +const schema = z.object({ userId: z.string().min(1) }); + +/** Transfers ownership to an existing admin; the previous owner becomes an admin. */ +export const POST = route<{ org: string }>(async (req: NextRequest, { params }) => { + const me = await requireUser(req); + const { org, role } = await loadOrg(params.org, me); + assertOrgOwner(role); + const { userId } = await parseBody(req, schema); + + if (userId === me.id) throw badRequest("You already own this organization."); + const target = await OrgMembership.findOne({ org: org._id, user: userId }).populate("user", "name email"); + if (!target) throw badRequest("That person is not a member of this organization."); + if (target.role !== "admin") throw badRequest("Make them an admin first, then transfer ownership.", "not_admin"); + + await OrgMembership.updateOne({ _id: target._id }, { role: "owner" }); + await OrgMembership.updateOne({ org: org._id, user: me.id }, { role: "admin" }); + org.owner = target.user._id; + await org.save(); + + await audit(org, me, "org.transfer", { type: "user", id: userId, name: target.user.email }, undefined, req); + return json({ message: `Ownership transferred to ${target.user.name}.` }); +}); diff --git a/web-app/src/app/api/orgs/route.ts b/web-app/src/app/api/orgs/route.ts new file mode 100644 index 0000000..8bcc075 --- /dev/null +++ b/web-app/src/app/api/orgs/route.ts @@ -0,0 +1,49 @@ +import { NextRequest } from "next/server"; +import { z } from "zod"; +import Organization from "@/models/organization"; +import OrgMembership from "@/models/orgMembership"; +import { requireUser } from "@/lib/auth"; +import { audit } from "@/lib/audit"; +import { json, parseBody, route } from "@/lib/http"; +import { nameSchema, slugSchema, uniqueSlug } from "@/lib/validation"; +import { conflict } from "@/lib/http"; +import { afterFilter, readPage, sortSpec, toPage, type SortField } from "@/lib/pagination"; + +const ORG_SORT: SortField[] = [{ key: "_id", dir: 1, type: "id" }]; + +/** Organizations the caller belongs to, with their role, oldest membership first. Paginated. */ +export const GET = route(async (req: NextRequest) => { + const me = await requireUser(req); + const { limit, after } = readPage(req, ORG_SORT); + const rows = await OrgMembership.find({ user: me.id, ...afterFilter(ORG_SORT, after) }) + .populate("org", "name slug") + .sort(sortSpec(ORG_SORT)) + .limit(limit + 1); + const { items, nextCursor } = toPage(rows as any[], limit, (m) => [m._id]); + return json({ + orgs: items.filter((m: any) => m.org).map((m: any) => ({ id: m.org._id, name: m.org.name, slug: m.org.slug, role: m.role })), + nextCursor, + }); +}); + +const createSchema = z.object({ name: nameSchema, slug: slugSchema.optional() }); + +export const POST = route(async (req: NextRequest) => { + const me = await requireUser(req); + const { name, slug: requested } = await parseBody(req, createSchema); + + let slug: string; + if (requested) { + if (await Organization.exists({ slug: requested })) throw conflict("That URL is already taken.", "slug_taken"); + slug = requested; + } else { + slug = await uniqueSlug(name, (s) => Organization.exists({ slug: s })); + } + + // New organizations start without the 2FA requirement; the owner can turn it on in settings. + const org = await Organization.create({ name, slug, owner: me.id, require2fa: false }); + await OrgMembership.create({ org: org._id, user: me.id, role: "owner" }); + await audit(org, me, "org.create", { type: "org", id: String(org._id), name }); + + return json({ org: { id: org._id, name: org.name, slug: org.slug, role: "owner" } }, 201); +}); diff --git a/web-app/src/app/api/passkeys/[id]/route.ts b/web-app/src/app/api/passkeys/[id]/route.ts new file mode 100644 index 0000000..b3a6f8f --- /dev/null +++ b/web-app/src/app/api/passkeys/[id]/route.ts @@ -0,0 +1,40 @@ +import { NextRequest } from "next/server"; +import { z } from "zod"; +import { isValidObjectId } from "mongoose"; +import { requireUser } from "@/lib/auth"; +import { json, notFound, parseBody, route } from "@/lib/http"; +import { assertCanRemoveFactor, clearIfNoFactorsLeft, syncPasskeyCount } from "@/lib/passkeys"; +import Passkey from "@/models/passkey"; +import User from "@/models/user"; + +type P = { id: string }; + +async function own(meId: string, id: string) { + if (!isValidObjectId(id)) throw notFound("Passkey not found."); + const key = await Passkey.findOne({ _id: id, user: meId }); + if (!key) throw notFound("Passkey not found."); + return key; +} + +const patchSchema = z.object({ name: z.string().trim().min(1, "Give it a name.").max(64) }); + +export const PATCH = route

(async (req: NextRequest, { params }) => { + const me = await requireUser(req); + const key = await own(me.id, params.id); + const { name } = await parseBody(req, patchSchema); + key.name = name; + await key.save(); + return json({ passkey: { id: key._id, name: key.name } }); +}); + +/** Removes a passkey (not the last second factor when one of the user's orgs requires 2FA). */ +export const DELETE = route

(async (req: NextRequest, { params }) => { + const me = await requireUser(req); + const key = await own(me.id, params.id); + const user = await User.findById(me.id).select("totpEnabled passkeyCount"); + await assertCanRemoveFactor(me.id, (user?.totpEnabled ? 1 : 0) + Math.max(0, (user?.passkeyCount ?? 0) - 1)); + await key.deleteOne(); + await syncPasskeyCount(me.id); + await clearIfNoFactorsLeft(me.id); + return json({ message: "Passkey removed." }); +}); diff --git a/web-app/src/app/api/passkeys/auth/options/route.ts b/web-app/src/app/api/passkeys/auth/options/route.ts new file mode 100644 index 0000000..15cbfbd --- /dev/null +++ b/web-app/src/app/api/passkeys/auth/options/route.ts @@ -0,0 +1,21 @@ +import { NextRequest } from "next/server"; +import { generateAuthenticationOptions } from "@simplewebauthn/server"; +import { requirePartialUser } from "@/lib/auth"; +import { badRequest, json, route } from "@/lib/http"; +import { passkeysFor, relyingParty, saveChallenge, transportsOf } from "@/lib/passkeys"; + +/** Options for the browser's "use your passkey" prompt (sign-in second step, unlocking values…). */ +export const POST = route(async (req: NextRequest) => { + const { user } = await requirePartialUser(req); + const keys = await passkeysFor(user._id); + if (!keys.length) throw badRequest("There are no passkeys on this account.", "no_passkeys"); + + const { rpID } = relyingParty(req); + const options = await generateAuthenticationOptions({ + rpID, + allowCredentials: keys.map((p: any) => ({ id: p.credentialId, transports: transportsOf(p) })), + userVerification: "preferred", + }); + await saveChallenge(user._id, options.challenge); + return json(options); +}); diff --git a/web-app/src/app/api/passkeys/auth/verify/route.ts b/web-app/src/app/api/passkeys/auth/verify/route.ts new file mode 100644 index 0000000..826147c --- /dev/null +++ b/web-app/src/app/api/passkeys/auth/verify/route.ts @@ -0,0 +1,73 @@ +import { NextRequest } from "next/server"; +import { z } from "zod"; +import { verifyAuthenticationResponse } from "@simplewebauthn/server"; +import { isoBase64URL } from "@simplewebauthn/server/helpers"; +import { requirePartialUser } from "@/lib/auth"; +import { HttpError, badRequest, forbidden, json, parseBody, route } from "@/lib/http"; +import { relyingParty, takeChallenge, transportsOf } from "@/lib/passkeys"; +import { REVEAL_TTL_MS } from "@/lib/reveal"; +import { generateBackupCodes, generateMfaTicket } from "@/lib/totp"; +import { mfaLimiter } from "@/utils/limiter"; +import Passkey from "@/models/passkey"; +import User from "@/models/user"; + +const schema = z.object({ + response: z.looseObject({ id: z.string().min(1).max(1024), type: z.literal("public-key") }), + /** + * What the passkey confirms: the second step of signing in, unlocking secret values for 10 minutes, + * or generating new backup codes. The last two need a session that already passed 2FA. + */ + purpose: z.enum(["login", "reveal", "backup-codes"]), +}); + +export const POST = route(async (req: NextRequest) => { + const { user, jwt } = await requirePartialUser(req); + const { response, purpose } = await parseBody(req, schema); + if (purpose !== "login" && !jwt?.mfaVerified) throw forbidden("Finish signing in first.", "mfa_required"); + + const limiterKey = `passkey:${user._id}`; + try { + await mfaLimiter.consume(limiterKey); + } catch { + throw new HttpError(429, "Too many attempts. Wait a few minutes and try again."); + } + + const expectedChallenge = await takeChallenge(user._id); + if (!expectedChallenge) throw badRequest("That took too long. Try again.", "challenge_expired"); + + const key: any = await Passkey.findOne({ user: user._id, credentialId: response.id }); + if (!key) throw badRequest("That passkey isn't registered to this account.", "unknown_passkey"); + + const { rpID, origin } = relyingParty(req); + let verification; + try { + verification = await verifyAuthenticationResponse({ + response: response as any, + expectedChallenge, + expectedOrigin: origin, + expectedRPID: rpID, + credential: { id: key.credentialId, publicKey: isoBase64URL.toBuffer(key.publicKey), counter: key.counter, transports: transportsOf(key) }, + requireUserVerification: false, + }); + } catch (e: any) { + throw badRequest(`Couldn't verify the passkey: ${e?.message ?? "unknown error"}`, "passkey_invalid"); + } + if (!verification.verified) throw badRequest("Couldn't verify the passkey.", "passkey_invalid"); + + await mfaLimiter.delete(limiterKey); + await Passkey.updateOne({ _id: key._id }, { $set: { counter: verification.authenticationInfo.newCounter, lastUsedAt: new Date() } }); + + if (purpose === "reveal") { + const revealUntil = new Date(Date.now() + REVEAL_TTL_MS); + await User.updateOne({ _id: user._id }, { $set: { revealUntil } }); + return json({ revealUntil }); + } + if (purpose === "backup-codes") { + const { codes, hashes } = generateBackupCodes(); + await User.updateOne({ _id: user._id }, { $set: { backupCodes: hashes } }); + return json({ backupCodes: codes }); + } + const ticket = generateMfaTicket(); + await User.updateOne({ _id: user._id }, { $set: { mfaTicketHash: ticket.hash, mfaTicketExpiry: ticket.expiry } }); + return json({ mfaTicket: ticket.ticket }); +}); diff --git a/web-app/src/app/api/passkeys/register/options/route.ts b/web-app/src/app/api/passkeys/register/options/route.ts new file mode 100644 index 0000000..a095d62 --- /dev/null +++ b/web-app/src/app/api/passkeys/register/options/route.ts @@ -0,0 +1,32 @@ +import { NextRequest } from "next/server"; +import { generateRegistrationOptions } from "@simplewebauthn/server"; +import { isoUint8Array } from "@simplewebauthn/server/helpers"; +import { requirePartialUser } from "@/lib/auth"; +import { badRequest, json, route } from "@/lib/http"; +import { assertCanEnroll, passkeysFor, relyingParty, saveChallenge, transportsOf } from "@/lib/passkeys"; + +const MAX_PASSKEYS = 10; + +/** Step 1 of adding a passkey: options for the browser's "create a passkey" prompt. */ +export const POST = route(async (req: NextRequest) => { + const { user, jwt } = await requirePartialUser(req); + assertCanEnroll(user, jwt); + + const existing = await passkeysFor(user._id); + if (existing.length >= MAX_PASSKEYS) throw badRequest(`You can have up to ${MAX_PASSKEYS} passkeys. Remove one first.`); + + const { rpID, rpName } = relyingParty(req); + const options = await generateRegistrationOptions({ + rpName, + rpID, + userName: user.email, + userDisplayName: user.name, + userID: isoUint8Array.fromUTF8String(String(user._id)), + attestationType: "none", + // Don't let the same authenticator register twice. + excludeCredentials: existing.map((p: any) => ({ id: p.credentialId, transports: transportsOf(p) })), + authenticatorSelection: { residentKey: "preferred", userVerification: "preferred" }, + }); + await saveChallenge(user._id, options.challenge); + return json(options); +}); diff --git a/web-app/src/app/api/passkeys/register/verify/route.ts b/web-app/src/app/api/passkeys/register/verify/route.ts new file mode 100644 index 0000000..dd4948b --- /dev/null +++ b/web-app/src/app/api/passkeys/register/verify/route.ts @@ -0,0 +1,85 @@ +import { NextRequest } from "next/server"; +import { z } from "zod"; +import { verifyRegistrationResponse } from "@simplewebauthn/server"; +import { isoBase64URL } from "@simplewebauthn/server/helpers"; +import { requirePartialUser } from "@/lib/auth"; +import { badRequest, json, parseBody, route } from "@/lib/http"; +import { assertCanEnroll, defaultPasskeyName, isMfaEnrolled, relyingParty, syncPasskeyCount, takeChallenge } from "@/lib/passkeys"; +import { generateBackupCodes, generateMfaTicket } from "@/lib/totp"; +import Passkey from "@/models/passkey"; +import User from "@/models/user"; + +const schema = z.object({ + response: z.looseObject({ id: z.string().min(1).max(1024), type: z.literal("public-key") }), + name: z.string().trim().max(64).optional(), +}); + +/** + * Step 2 of adding a passkey: checks the browser's response and stores the public key. When this is the + * account's first second factor (sign-up), it also returns backup codes and an MFA ticket for the session. + */ +export const POST = route(async (req: NextRequest) => { + const { user, jwt } = await requirePartialUser(req); + assertCanEnroll(user, jwt); + const { response, name } = await parseBody(req, schema); + + const expectedChallenge = await takeChallenge(user._id); + if (!expectedChallenge) throw badRequest("That took too long. Try adding the passkey again.", "challenge_expired"); + + const { rpID, origin } = relyingParty(req); + let verification; + try { + verification = await verifyRegistrationResponse({ + response: response as any, + expectedChallenge, + expectedOrigin: origin, + expectedRPID: rpID, + requireUserVerification: false, + }); + } catch (e: any) { + throw badRequest(`Couldn't verify the passkey: ${e?.message ?? "unknown error"}`, "passkey_invalid"); + } + if (!verification.verified || !verification.registrationInfo) throw badRequest("Couldn't verify the passkey.", "passkey_invalid"); + + const { credential, credentialDeviceType, credentialBackedUp } = verification.registrationInfo; + if (await Passkey.exists({ credentialId: credential.id })) throw badRequest("This passkey is already registered.", "passkey_exists"); + + const firstFactor = !isMfaEnrolled(user); + const passkey = await Passkey.create({ + user: user._id, + credentialId: credential.id, + publicKey: isoBase64URL.fromBuffer(credential.publicKey), + counter: credential.counter, + transports: credential.transports ?? [], + deviceType: credentialDeviceType, + backedUp: credentialBackedUp, + name: name || defaultPasskeyName(req), + }); + await syncPasskeyCount(user._id); + + // First-time setup: backup codes for when the passkey isn't at hand, and a ticket to finish signing in. + let backupCodes: string[] | null = null; + let mfaTicket: string | null = null; + const set: Record = {}; + if (firstFactor) { + const generated = generateBackupCodes(); + backupCodes = generated.codes; + set.backupCodes = generated.hashes; + } + if (!jwt?.mfaVerified) { + const ticket = generateMfaTicket(); + mfaTicket = ticket.ticket; + set.mfaTicketHash = ticket.hash; + set.mfaTicketExpiry = ticket.expiry; + } + if (Object.keys(set).length) await User.updateOne({ _id: user._id }, { $set: set }); + + return json( + { + passkey: { id: passkey._id, name: passkey.name, createdAt: passkey.createdAt, backedUp: passkey.backedUp }, + backupCodes, + mfaTicket, + }, + 201 + ); +}); diff --git a/web-app/src/app/api/passkeys/route.ts b/web-app/src/app/api/passkeys/route.ts new file mode 100644 index 0000000..ba21d28 --- /dev/null +++ b/web-app/src/app/api/passkeys/route.ts @@ -0,0 +1,19 @@ +import { NextRequest } from "next/server"; +import { requireUser } from "@/lib/auth"; +import { json, route } from "@/lib/http"; +import { passkeysFor } from "@/lib/passkeys"; + +/** The signed-in user's passkeys (names and dates only). */ +export const GET = route(async (req: NextRequest) => { + const me = await requireUser(req); + const keys = await passkeysFor(me.id); + return json({ + passkeys: keys.map((p: any) => ({ + id: p._id, + name: p.name, + createdAt: p.createdAt, + lastUsedAt: p.lastUsedAt ?? null, + backedUp: !!p.backedUp, + })), + }); +}); diff --git a/web-app/src/app/api/register/route.ts b/web-app/src/app/api/register/route.ts new file mode 100644 index 0000000..ee42c23 --- /dev/null +++ b/web-app/src/app/api/register/route.ts @@ -0,0 +1,53 @@ +import { NextRequest } from "next/server"; +import { z } from "zod"; +import User from "@/models/user"; +import { hashPassword } from "@/utils/bcrypt"; +import { json, parseBody, rateLimit, route, conflict, forbidden } from "@/lib/http"; +import { emailSchema, nameSchema, passwordSchema } from "@/lib/validation"; + +const schema = z.object({ + name: nameSchema, + username: z + .string() + .trim() + .toLowerCase() + .regex(/^[a-z0-9_.-]{3,32}$/, "Username must be 3–32 characters: letters, numbers, _ . -"), + email: emailSchema, + password: passwordSchema, +}); + +/** + * Email + password sign-up is closed: new accounts are created with Google, and people can set a password + * in Account settings afterwards. Kept for local development and automated tests only. + */ +const passwordSignupAllowed = () => process.env.ALLOW_PASSWORD_SIGNUP === "true" || process.env.NODE_ENV === "development"; + +export const POST = route(async (req: NextRequest) => { + await rateLimit(req); + if (!passwordSignupAllowed()) { + throw forbidden("Create your account with Google. You can set a password in Account settings afterwards.", "google_only"); + } + const { name, username, email, password } = await parseBody(req, schema); + + const existing = await User.findOne({ $or: [{ email }, { username }] }).select("email username"); + if (existing) { + throw conflict( + existing.email === email + ? "An account with this email already exists." + : "That username is taken.", + "account_exists" + ); + } + + await User.create({ + name, + username, + email, + password: await hashPassword(password), + provider: "credentials", + // No email service yet: password accounts stay unverified and can only join orgs via invite links. + emailVerified: false, + }); + + return json({ message: "Account created." }, 201); +}); diff --git a/web-app/src/app/api/share/[token]/route.ts b/web-app/src/app/api/share/[token]/route.ts new file mode 100644 index 0000000..f65fdec --- /dev/null +++ b/web-app/src/app/api/share/[token]/route.ts @@ -0,0 +1,36 @@ +import { NextRequest } from "next/server"; +import BasicGroup from "@/models/basicGroup"; +import { json, notFound, rateLimit, route } from "@/lib/http"; +import { decrypt } from "@/utils/encryption"; +import { MAX_SHARE_TTL_SECONDS, tokenExpiry } from "@/lib/share"; + +/** 410 with the expiry time, so the page can say when the link expired. */ +const expired = (expiredAt: Date) => + json({ message: "This link has expired and its variables were deleted.", code: "expired", expiredAt }, 410); + +export const GET = route<{ token: string }>(async (req: NextRequest, { params }) => { + await rateLimit(req); + const share: any = await BasicGroup.findOne({ token: params.token }); + if (!share) { + // Already deleted (by the TTL index or an earlier visit): the token itself says whether it expired. + const expiry = tokenExpiry(params.token); + if (expiry && expiry.getTime() <= Date.now()) return expired(expiry); + throw notFound("This link doesn't exist. Check that it was copied in full."); + } + + // Expire on the stored date, and never later than 24 hours after creation, which also + // retires older links that were created with longer lifetimes. The TTL index can lag, + // so expired links are deleted here as soon as someone tries to open them. + const created = share.createdAt?.getTime?.() ?? share.expireAt.getTime(); + const hardLimit = created + MAX_SHARE_TTL_SECONDS * 1000; + const expiresAt = Math.min(share.expireAt.getTime(), hardLimit); + if (expiresAt <= Date.now()) { + await BasicGroup.deleteOne({ _id: share._id }); + return expired(new Date(expiresAt)); + } + + return json({ + expiresAt: new Date(expiresAt), + vars: (share.envs ?? []).map((e: any) => ({ key: e.key, value: decrypt(e.value) })), + }); +}); diff --git a/web-app/src/app/api/share/route.ts b/web-app/src/app/api/share/route.ts new file mode 100644 index 0000000..49698dc --- /dev/null +++ b/web-app/src/app/api/share/route.ts @@ -0,0 +1,34 @@ +import { NextRequest } from "next/server"; +import { z } from "zod"; +import { randomBytes } from "crypto"; +import BasicGroup from "@/models/basicGroup"; +import { json, parseBody, rateLimit, route } from "@/lib/http"; +import { envVarsSchema } from "@/lib/validation"; +import { encrypt } from "@/utils/encryption"; +import { SHARE_TTL_OPTIONS, withExpiry } from "@/lib/share"; + +const ALLOWED_TTLS = SHARE_TTL_OPTIONS.map((o) => o.seconds) as number[]; + +const schema = z.object({ + vars: envVarsSchema.refine((v) => v.length > 0, "Add at least one variable."), + expiresInSeconds: z + .number() + .int() + .refine((n) => ALLOWED_TTLS.includes(n), "Links can last 10 minutes, 1 hour or 24 hours."), +}); + +/** Temporary share link (no account required). Always expires, at most 24 hours after creation. */ +export const POST = route(async (req: NextRequest) => { + await rateLimit(req); + const { vars, expiresInSeconds } = await parseBody(req, schema); + + const expireAt = new Date(Date.now() + expiresInSeconds * 1000); + const token = withExpiry(randomBytes(18).toString("base64url"), expireAt); + const share = await BasicGroup.create({ + token, + envs: vars.map(({ key, value }) => ({ key, value: encrypt(value) })), + expireAt, + }); + + return json({ token: share.token, expiresAt: share.expireAt }, 201); +}); diff --git a/web-app/src/app/api/tokens/[id]/route.ts b/web-app/src/app/api/tokens/[id]/route.ts new file mode 100644 index 0000000..eaf8432 --- /dev/null +++ b/web-app/src/app/api/tokens/[id]/route.ts @@ -0,0 +1,11 @@ +import { NextRequest } from "next/server"; +import ApiToken from "@/models/apiToken"; +import { requireUser } from "@/lib/auth"; +import { json, notFound, route } from "@/lib/http"; + +export const DELETE = route<{ id: string }>(async (req: NextRequest, { params }) => { + const me = await requireUser(req); + const result = await ApiToken.updateOne({ _id: params.id, user: me.id, revokedAt: null }, { revokedAt: new Date() }); + if (!result.matchedCount) throw notFound("Token not found."); + return json({ message: "Token revoked." }); +}); diff --git a/web-app/src/app/api/tokens/route.ts b/web-app/src/app/api/tokens/route.ts new file mode 100644 index 0000000..5c8aef0 --- /dev/null +++ b/web-app/src/app/api/tokens/route.ts @@ -0,0 +1,66 @@ +import { NextRequest } from "next/server"; +import { z } from "zod"; +import ApiToken from "@/models/apiToken"; +import { generatePat, requireUser } from "@/lib/auth"; +import { badRequest, json, parseBody, route } from "@/lib/http"; +import { nameSchema } from "@/lib/validation"; +import { afterFilter, readPage, sortSpec, toPage, type SortField } from "@/lib/pagination"; + +const TOKEN_SORT: SortField[] = [{ key: "_id", dir: -1, type: "id" }]; + +/** The caller's active tokens, newest first. Paginated. */ +export const GET = route(async (req: NextRequest) => { + const me = await requireUser(req); + const { limit, after } = readPage(req, TOKEN_SORT); + const rows = await ApiToken.find({ user: me.id, revokedAt: null, ...afterFilter(TOKEN_SORT, after) }) + .sort(sortSpec(TOKEN_SORT)) + .limit(limit + 1); + const { items, nextCursor } = toPage(rows as any[], limit, (t) => [t._id]); + return json({ + nextCursor, + tokens: items.map((t: any) => ({ + id: t._id, + name: t.name, + prefix: t.prefix, + createdAt: t.createdAt, + lastUsedAt: t.lastUsedAt ?? null, + expiresAt: t.expiresAt ?? null, + })), + }); +}); + +const DAY = 24 * 60 * 60 * 1000; + +/** Expiry: a preset number of days, a custom date (`expiresAt`, up to a year ahead), or never (`expiresInDays: null`). */ +const schema = z + .object({ + name: nameSchema, + expiresInDays: z.number().int().min(1).max(365).nullable().default(90), + expiresAt: z.iso.datetime({ offset: true }).optional(), + }) + .refine((v) => !v.expiresAt || (Date.parse(v.expiresAt) > Date.now() && Date.parse(v.expiresAt) <= Date.now() + 366 * DAY), { + message: "Pick an expiry date between tomorrow and one year from now.", + path: ["expiresAt"], + }); + +/** Creates a personal access token. The plaintext is returned exactly once. */ +export const POST = route(async (req: NextRequest) => { + const me = await requireUser(req); + if (me.via === "token") throw badRequest("API tokens can't create other tokens."); + const { name, expiresInDays, expiresAt } = await parseBody(req, schema); + + if ((await ApiToken.countDocuments({ user: me.id, revokedAt: null })) >= 25) { + throw badRequest("You can have at most 25 active tokens. Revoke one first."); + } + + const { token, hash, prefix } = generatePat(); + const doc = await ApiToken.create({ + user: me.id, + name, + prefix, + tokenHash: hash, + expiresAt: expiresAt ? new Date(expiresAt) : expiresInDays ? new Date(Date.now() + expiresInDays * DAY) : undefined, + }); + + return json({ token, id: doc._id, prefix, expiresAt: doc.expiresAt ?? null }, 201); +}); diff --git a/web-app/src/app/api/v1/[org]/[team]/[project]/[env]/route.ts b/web-app/src/app/api/v1/[org]/[team]/[project]/[env]/route.ts new file mode 100644 index 0000000..de284ea --- /dev/null +++ b/web-app/src/app/api/v1/[org]/[team]/[project]/[env]/route.ts @@ -0,0 +1,43 @@ +import { NextRequest, NextResponse } from "next/server"; +import { requireUser } from "@/lib/auth"; +import { loadEnvironment, loadOrg, loadProject, loadTeam } from "@/lib/authz"; +import { json, route } from "@/lib/http"; +import { stringifyDotenv } from "@/lib/dotenv"; +import { decrypt } from "@/utils/encryption"; +import { assertCanSeeValues } from "@/lib/reveal"; + +type P = { org: string; team: string; project: string; env: string }; + +/** + * CLI/SDK endpoint. Authenticate with `Authorization: Bearer envoy_pat_…`. + * Access is evaluated live against the token owner's current org/team role. + * ?format=json (default) → { vars: { KEY: "value" } } + * ?format=dotenv → text/plain .env file + */ +export const GET = route

(async (req: NextRequest, { params }) => { + const me = await requireUser(req, { allowToken: true }); + const ctx = await loadEnvironment( + await loadProject(await loadTeam(await loadOrg(params.org, me), params.team, me), params.project), + params.env + ); + // Tokens are fine; a browser session must have unlocked values like everywhere else. + await assertCanSeeValues(me); + + const vars = (ctx.environment.vars ?? []).map((v: any) => ({ key: v.key, value: decrypt(v.value) })); + + if (req.nextUrl.searchParams.get("format") === "dotenv") { + return new NextResponse(stringifyDotenv(vars), { + status: 200, + headers: { "Content-Type": "text/plain; charset=utf-8", "Cache-Control": "no-store" }, + }); + } + + return json({ + org: ctx.org.slug, + team: ctx.team.slug, + project: ctx.project.slug, + environment: ctx.environment.slug, + version: ctx.environment.version ?? 0, + vars: Object.fromEntries(vars.map((v: { key: string; value: string }) => [v.key, v.value])), + }); +}); diff --git a/web-app/src/app/dashboard/[uuid]/[token]/page.tsx b/web-app/src/app/dashboard/[uuid]/[token]/page.tsx deleted file mode 100644 index 5f53716..0000000 --- a/web-app/src/app/dashboard/[uuid]/[token]/page.tsx +++ /dev/null @@ -1,592 +0,0 @@ -"use client" - -import React, { useState, useEffect, useRef } from 'react' -import { useParams } from 'next/navigation' -import SidebarComp from '@/components/dashboard/sidebar'; -import LoadingPage from '@/components/misc/loadingPage'; -import Button from '@/components/button'; -import { Copy, Plus, Trash2, Pencil, Save, PlusCircle, Clipboard, Lock, LockOpen, Users, LinkIcon, AlertTriangle, Globe } from 'lucide-react'; -import { useSession } from 'next-auth/react'; -import { toast } from 'sonner'; -import DialogBox from '@/components/dialogBox'; -import { decrypt } from '@/utils/encryption'; -import getEnvVarsFromClipboard from '@/utils/getEnvFromClipbaord'; - -const GroupPage = () => { - - const { token } = useParams(); - const [group, setGroup] = useState(null); - const [loading, setLoading] = useState(true); - const [isEditing, setIsEditing] = useState(false); - const [dialogOpen, setDialogOpen] = useState(false); - const [newMemberEmail, setNewMemberEmail] = useState(''); - const prevIsEditingRef = useRef(isEditing); - const [originalEnvs, setOriginalEnvs] = useState([]); - const [loading2, setLoading2] = useState(); - - - const { data: session, status } = useSession(); - - useEffect(() => { - - if (status !== "authenticated") return; - - const fetchGroup = async () => { - setLoading(true); - try { - const res = await fetch(`/api/group?token=${token}&edit=true`, { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - }, - }); - - const data = await res.json(); - - if (!res.ok) { - throw Error(data.message || 'Failed to fetch group data.'); - } - - - // Strip `_id` from envs - const cleanEnvs = (data.group?.envs || []).map(({ key, value }: any) => ({ - key, - value, - })); - - const decryptedEnvs = cleanEnvs.map((env: any) => ({ - ...env, - value: decrypt(env.value), - })); - - setGroup({ - ...data.group, - envs: decryptedEnvs, - }); - setOriginalEnvs(decryptedEnvs); - - } catch (err: any) { - toast.error(err.message); - } finally { - setLoading(false); - } - }; - - fetchGroup(); - }, [token, status]); - - const haveEnvsChanged = (envs: any[], original: any[]) => { - if (envs.length !== original.length) return true; - for (let i = 0; i < envs.length; i++) { - if (envs[i].key !== original[i].key || envs[i].value !== original[i].value) { - return true; - } - } - return false; - }; - - - const handleChangeVisibility = async () => { - setLoading2(true); - try { - const res = await fetch(`/api/group/visibility`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - groupToken: group?.token - }), - }); - - const data = await res.json(); - - if (!res.ok) { - toast.error(data.message || 'Failed to change visibility.'); - return; - } - - setGroup((prevGroup: any) => ({ - ...prevGroup, - isPublic: !prevGroup.isPublic, - })); - - toast.success(data.message || 'Visibility updated successfully.'); - } catch (err) { - toast.error('Something went wrong while changing visibility.'); - } finally { - setLoading2(false); - } - } - - const handlePasteEnvs = async () => { - const newEnvs = await getEnvVarsFromClipboard(); - - if (newEnvs.length === 0) { - toast.error("No valid envs found in clipboard."); - return; - } - - toast("Paste envs?", { - description: `${newEnvs.length} variable${newEnvs.length > 1 ? 's' : ''} detected.`, - action: { - label: "Yes", - onClick: () => { - const merged = [...(group.envs ?? []), ...newEnvs]; - - // Deduplicate by key (last one wins) - const deduped = Array.from( - new Map(merged.map((env) => [env.key, env])).values() - ); - - setGroup({ ...group, envs: deduped }); - - toast.success("Variables updated."); - }, - }, - }); - }; - - - const handleEnvChange = (index: number, field: 'key' | 'value', value: string) => { - setGroup((prevGroup: any) => { - const updatedEnvs = [...prevGroup.envs]; - updatedEnvs[index] = { ...updatedEnvs[index], [field]: value }; - return { ...prevGroup, envs: updatedEnvs }; - }); - }; - - const handleAddEnv = () => { - setGroup((prevGroup: any) => ({ - ...prevGroup, - envs: [...prevGroup.envs, { key: '', value: '' }], - })); - }; - - const handleRemoveEnv = (index: number) => { - setGroup((prevGroup: any) => { - const updatedEnvs = [...prevGroup.envs]; - updatedEnvs.splice(index, 1); - return { ...prevGroup, envs: updatedEnvs }; - }); - }; - - const handleCopyEnvs = () => { - if (!group?.envs || group.envs.length === 0) return; - - const formatted = group.envs - .filter((env: any) => env.key?.trim()) - .map((env: any) => `${env.key}=${env.value}`) - .join('\n'); - - navigator.clipboard.writeText(formatted) - .then(() => { - toast.success("Copied to clipboard!"); - }) - .catch((err) => { - toast.error("Failed to copy:", err); - }); - }; - - const handleSaveChanges = async () => { - const envsChanged = haveEnvsChanged(group.envs, originalEnvs); - if (!envsChanged) { - setIsEditing(false); - return; - } - - setLoading(true); - try { - const res = await fetch(`/api/env`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - groupToken: token, - envs: group.envs, - }), - }); - - const data = await res.json(); - - if (!res.ok) { - toast.error(data.message || 'Failed to save changes.'); - setTimeout(() => { window.location.reload() }, 500); - return; - } - - toast.success(data.message || 'Changes saved successfully.'); - setOriginalEnvs(group.envs); - } catch (err) { - console.log('Error saving changes:', err); - toast.error('Something went wrong while saving.'); - } finally { - setLoading(false); - setIsEditing(false); - } - }; - - - - useEffect(() => { - if (prevIsEditingRef.current && !isEditing) { - handleSaveChanges(); - } - prevIsEditingRef.current = isEditing; - }, [isEditing]); - - - const getMembers = async () => { - try { - const res = await fetch(`/api/group/member/get?token=${token}`, { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - }, - }); - - if (!res.ok) { - toast.error('Failed to fetch members.'); - return; - } - - const data = await res.json(); - setGroup((prevGroup: any) => ({ - ...prevGroup, - members: data.members || [], - })); - } catch (err) { - toast.error('Error fetching members.'); - } finally { - } - } - - const handleAddMember = async () => { - try { - setLoading2(true); - setDialogOpen(false); - const res = await fetch('/api/group/member/add', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - groupId: group._id, - memberEmail: newMemberEmail, - }), - }) - - if (!res.ok) { - const data = await res.json(); - toast.error(data.message || 'Failed to add member.'); - return; - } - - const updatedGroup = await res.json(); - toast.success(updatedGroup.message || 'Member added successfully.'); - - } catch (err) { - toast.error('Error adding member.'); - } finally { - getMembers().then(() => { - setLoading2(false); - }); - setNewMemberEmail(''); - } - } - - const handleRemoveMember = async (memberEmail: string) => { - try { - setLoading2(true); - const res = await fetch(`/api/group/member/remove`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - groupId: group._id, - memberEmail, - }), - }); - - if (!res.ok) { - const data = await res.json(); - toast.error(data.message || 'Failed to remove member.'); - return; - } - - const updatedGroup = await res.json(); - toast.success(updatedGroup.message || 'Member removed successfully.'); - } catch (err) { - toast.error('Error removing member.'); - } finally { - getMembers().then(() => { - setLoading2(false); - }) - } - } - - const handleCopyLink = () => { - if (!group?.token) return; - - const link = `${window.location.origin}/group?token=${group.token}`; - navigator.clipboard.writeText(link) - .then(() => { - toast.success("Link copied to clipboard!"); - }) - .catch((err) => { - toast.error("Failed to copy link:", err); - }); - }; - - const getInitials = (name: string = '') => - name - .split(' ') - .map((n) => n[0]) - .slice(0, 2) - .join('') - .toUpperCase(); - - - if (loading || !session) { - return ; - } - - - return ( - <> -

- - -
- {group && ( -
-
-
- -
-
-

{group.name}

-

- Created on {new Date(group.createdAt).toLocaleDateString("en-GB", { - day: "numeric", - month: "long", - year: "numeric", - })} -

-
-
- -
- {group.isPublic && ( - - )} - -
-
- )} - -
- - {!group?.envs && ( -
-
-
- -
-

- Group Not Found -

-

- The link may be broken, or the group may have been deleted. Please check the URL and try again. -

-
-
- )} - - {group?.envs && ( - <> -
-
-

- Environment Variables -

-
- {isEditing && ( - <> - - - - )} - {!isEditing && ( - - )} - - -
-
- -
- {group.envs?.length > 0 ? ( -
- {group.envs.map((env: any, index: number) => ( -
- handleEnvChange(index, 'key', e.target.value)} /> - = - - handleEnvChange(index, 'value', e.target.value)} /> - {isEditing && ( -
- -
- )} -
- ))} -
- ) : ( -

No Variables Yet

Click 'Edit' then the 'Add' button to create your first variable.

- )} -
-
- -
-

Members

- - - - {loading2 && ( - <> - {[...Array(4)].map((_, index) => ( -
-
-
-
-
-
-
-
-
- ))} - - )} - {group?.isPublic && !loading2 && ( - <> -
- -
- This is a public group - - It's visible to anyone with the link. - -
-
- - )} - - {!loading2 && group.members.map((member: any) => ( -
-
-
- {getInitials(member.name)} -
- -
- {member.name} - {member.email} -
-
- - {isEditing && ( - - )} -
- ))} -
- - )} -
-
-
- - - setDialogOpen(false)} - onAction={() => { handleAddMember() }} - loading={loading} - > -
- setNewMemberEmail(e.target.value)} - className="p-3 border-1 border-[#00916C] dark:border-[#00916C] rounded-xl bg-white dark:bg-[#1e1e1e] text-black dark:text-white focus:outline-none focus:ring-2 focus:ring-[#00916C] dark:focus:ring-[#00916C] transition" - required - /> -
- -
- - - ) -} - -export default GroupPage \ No newline at end of file diff --git a/web-app/src/app/dashboard/[uuid]/page.tsx b/web-app/src/app/dashboard/[uuid]/page.tsx deleted file mode 100644 index 79f1aef..0000000 --- a/web-app/src/app/dashboard/[uuid]/page.tsx +++ /dev/null @@ -1,312 +0,0 @@ -'use client' - -import React, { useState, useEffect } from 'react' -import { useParams } from 'next/navigation'; -import SidebarComp from '@/components/dashboard/sidebar'; -import { ArrowRight, Trash2, Users, Briefcase, FolderPlus, Plus, CalendarDays } from 'lucide-react'; -import Link from 'next/link'; -import DialogBox from '@/components/dialogBox'; -import { toast } from 'sonner'; - -const GroupsPage = () => { - - const { uuid } = useParams(); - const [groups, setGroups] = useState([]); - const [loading, setLoading] = useState(true); - const [workspace, setWorkspace] = useState(null); - const [dialogOpen, setDialogOpen] = useState(false); - const [newGroupName, setNewGroupName] = useState(''); - - useEffect(() => { - - const fetchGroups = async () => { - try { - setLoading(true); - const res = await fetch(`/api/workspace?id=${uuid}`, - { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - }, - }); - - if (!res.ok) { - toast.error('Failed to fetch groups.'); - return null; - } - - const data = await res.json(); - setGroups(data.workspace.groups || []); - setWorkspace(data.workspace); - - } catch (err) { - toast.error('Error fetching groups.'); - } finally { - setLoading(false); - } - } - - fetchGroups(); - - }, []); - - const handleDeleteGroup = async (groupID: String) => { - - try { - setLoading(true); - const res = await fetch(`/api/group/delete`, { - method: 'DELETE', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - groupId: groupID, - }), - }); - - if (!res.ok) { - toast.error('Error deleting group. Please try again.'); - return; - } - - const updatedGroups = await fetch(`/api/workspace?id=${uuid}`, { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - }, - }); - const data = await updatedGroups.json(); - setGroups(data.workspace.groups || []); - toast.success('Group deleted successfully.'); - } catch (err) { - toast.error('Error deleting group.'); - } finally { - setLoading(false); - } - } - - const handleCreateGroup = async () => { - try { - setLoading(true); - const res = await fetch(`/api/group/create`, { - method: "POST", - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - workspaceId: workspace._id, - name: newGroupName, - }), - }); - - if (!res.ok) { - toast.error('Error creating group. Please try again.'); - } - - const newGroups = await fetch(`/api/workspace?id=${uuid}`, { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - }, - }); - const data = await newGroups.json(); - setGroups(data.workspace.groups || []); - setNewGroupName(''); - toast.success('Group created successfully.'); - - } catch (err) { - toast.error('Error creating group. Please try again.'); - } finally { - setDialogOpen(false); - setLoading(false); - } - - } - - return ( - <> -
- - -
- {!workspace && ( -
-
-
-
-
-
-
-
-
-
-
-
- {Array.from({ length: 4 }).map((_, i) => ( -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- ))} -
-
- )} - - {workspace && ( - <> -
-
-
- -
-
-

{workspace.name}

-

{workspace.description}

-
-
- -
- -
-
- {!groups && Array.from({ length: 4 }).map((_, i) => ( -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- ))} - - {groups && groups.length === 0 && ( -
-
-

Create your first group

-

Groups help you organize your variables and team members.

-
- )} - - {loading && ( - <> - {Array.from({ length: 4 }).map((_, i) => ( -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- ))} - - )} - - {!loading && groups && groups.length > 0 && ( - groups.map((group: any) => ( -
-
- -
-
-
-

{group.name}

-
-
-
- {group.members.length} Members - {new Date(group.createdAt).toLocaleDateString("en-GB", { month: "short", day: "numeric", year: "numeric" })} -
- -
- - -
-
-
- )) - )} -
-
- - )} -
-
- - {dialogOpen && ( - setDialogOpen(false)} - onAction={handleCreateGroup} - loading={loading} - isActionDisabled={!newGroupName.trim()} - > -
- - setNewGroupName(e.target.value)} - className="p-3 border border-gray-300 dark:border-gray-600 bg-white dark:bg-[#1E1F1F] text-black dark:text-white rounded-lg focus:outline-none focus:ring-2 focus:ring-[#00916C] focus:border-transparent transition-colors duration-200" - required - autoFocus - /> -
-
- )} - - - ) -} - -export default GroupsPage \ No newline at end of file diff --git a/web-app/src/app/dashboard/account/page.tsx b/web-app/src/app/dashboard/account/page.tsx deleted file mode 100644 index fe3f55c..0000000 --- a/web-app/src/app/dashboard/account/page.tsx +++ /dev/null @@ -1,168 +0,0 @@ -"use client" -import React, { useState, useEffect, ReactNode } from 'react' -import SidebarComp from '@/components/dashboard/sidebar' -import { toast } from 'sonner' -import { User, Mail, Briefcase, Users, Calendar, KeyRound, ShieldCheck } from 'lucide-react' -import Button from '@/components/button' - -interface UserProfile { - _id: string; - name: string; - email: string; - provider: 'google' | 'credentials' | string; - workspaces: string[]; - otherGroups: string[]; - createdAt: string; - updatedAt: string; -} - -interface InfoRowProps { - icon: ReactNode; - label: string; - value: string | number; -} - -const InfoRow: React.FC = ({ icon, label, value }) => ( -
-
- {icon} -
-
-

{label}

-

{value}

-
-
-); - -const InfoRowSkeleton: React.FC = () => ( -
-
-
-
-
-
-
-); - -const AccountPageSkeleton: React.FC = () => ( -
-
-
-
- - -
-
- -
-
-
- - - -
-
-
-
-
-
-
-); - -const MyAccountPage = () => { - - const [loading, setLoading] = useState(true) - const [user, setUser] = useState(null); - - useEffect(() => { - const fetchData = async () => { - try { - setLoading(true); - const res = await fetch('/api/me'); - const data = await res.json(); - if (!res.ok) { - toast.error(data.message || 'Failed to fetch user data'); - } - setUser(data.user); - } catch (error) { - console.error('Error fetching user data:', error); - toast.error('An error occurred while fetching your account information.'); - } finally { - setLoading(false); - } - } - fetchData(); - }, []) - - const formatDate = (dateString: string) => { - if (!dateString) return 'N/A'; - return new Date(dateString).toLocaleDateString('en-US', { - year: 'numeric', - month: 'long', - day: 'numeric', - }); - }; - - return ( - <> -
- -
-
-
- -
-
-

Account Information

-

- Manage your profile, workspace, and security settings. -

-
-
- - {loading ? ( - - ) : user && ( -
-
-

Profile Details

-
- } label="Name" value={user.name} /> - } label="Email" value={user.email} /> -
-
- -
-

Usage & History

-
- } label="Your Workspaces" value={`${user.workspaces.length} Created`} /> - } label="Shared Groups" value={`${user.otherGroups.length} Joined`} /> - } label="Account Created" value={formatDate(user.createdAt)} /> -
-
- -
-

Security

-
-
- -
-
-

Authentication Provider

-

- Your account is securely managed by{" "} - {user.provider}. -

-
-
-
-
- )} -
-
- - - ) -} - -export default MyAccountPage \ No newline at end of file diff --git a/web-app/src/app/dashboard/page.tsx b/web-app/src/app/dashboard/page.tsx index 51ff70b..a843e92 100644 --- a/web-app/src/app/dashboard/page.tsx +++ b/web-app/src/app/dashboard/page.tsx @@ -1,313 +1,25 @@ -"use client" -import React, { useState, useEffect, useRef } from 'react' -import SidebarComp from '@/components/dashboard/sidebar' -import { ArrowRight, FolderPlus, Trash2, CalendarDays, Users, Briefcase, Plus } from 'lucide-react' -import Link from 'next/link' -import DialogBox from '@/components/dialogBox' -import { toast } from 'sonner' - - -const DashboardPage = () => { - - const [workspaces, setWorkspaces] = useState([]); - const [loading, setLoading] = useState(true); - const workspace = useRef({ - name: "", - description: "", - }); - const [showModal, setShowModal] = useState(false); - const deleteWorkspace = useRef({ - id: "", - name: "", - }); - const [showDeleteModal, setShowDeleteModal] = useState(false); - - useEffect(() => { - const fetchWorkspaces = async () => { - setLoading(true); - try { - const res = await fetch("/api/workspace"); - - if (!res.ok) { - const errorText = await res.text(); - toast.error("Failed to fetch workspaces."); - return; - } - - const data = await res.json(); - setWorkspaces(data.workspaces); - } catch (err) { - toast.error("Failed to fetch workspaces."); - } finally { - setLoading(false); - } - }; - - fetchWorkspaces(); - }, []); - - const handleCreateWorkspace = async () => { - setLoading(true); - try { - const res = await fetch("/api/workspace/create", { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - name: workspace.current.name, - description: workspace.current.description, - }), - }); - - if (!res.ok) { - const errorText = await res.json(); - toast.error(errorText.message || "Failed to create workspace."); - return; - } - - const updatedRes = await fetch("/api/workspace"); - const updatedData = await updatedRes.json(); - setWorkspaces(updatedData.workspaces); - toast.success("Workspace created successfully."); - - } catch (err) { - toast.error("Error creating workspace."); - } finally { - setLoading(false); - setShowModal(false); - workspace.current.name = ""; - workspace.current.description = ""; - } - }; - - const handleDeleteWorkspace = async (workspaceId: string) => { - try{ - setLoading(true); - const res = await fetch("/api/workspace/delete", { - method: 'DELETE', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ workspaceId }), - }); - - if (!res.ok) { - toast.error("Failed to delete workspace."); - return; - } - - const updatedRes = await fetch("/api/workspace"); - const updatedData = await updatedRes.json(); - setWorkspaces(updatedData.workspaces); - toast.success(`Workspace deleted successfully.`); - } catch { - toast.error("Unexpected error occurred while deleting workspace.") - } finally { - setLoading(false); - workspace.current.name = ""; - workspace.current.description = ""; - } - }; - - return ( - <> -
- -
- -
-
-
- -
-
-

Workspaces

-

Your collaborative environments.

-
-
- -
- -
-
- {loading && ( - Array.from({ length: 8 }).map((_, i) => ( -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- )) - )} - - {/* EMPTY CASE */} - {!loading && workspaces && workspaces.length === 0 && ( -
-
- -
-

- Create your first workspace -

-

- Click the "New Workspace" button to get started. -

-
- )} - - {!loading && workspaces && workspaces.length > 0 && ( - workspaces.map((workspace: any) => ( -
-
- -
-
- -
-
-

- {workspace.name} -

-

- {workspace.description || 'No description available.'} -

-
-
-
- - - {workspace.groups?.length ?? 0} Groups - - - - {new Date(workspace.createdAt).toLocaleDateString("en-GB", { - month: "short", - day: "numeric", - year: "numeric" - })} - -
- -
- - - - -
-
-
- )) - )} -
-
-
-
- - {showModal && ( - setShowModal(false)} - onAction={handleCreateWorkspace} - loading={loading} - actionLabel="Submit" - > -
-
- - { - workspace.current.name = e.target.value; - }} - className="p-3 border border-gray-300 dark:border-gray-600 bg-white dark:bg-[#1E1F1F] text-black dark:text-white rounded-lg focus:outline-none focus:ring-2 focus:ring-[#00916C] focus:border-transparent transition-colors duration-200" - placeholder='Enter Workspace Name' - required - /> -
- -
- -