diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2d24ece --- /dev/null +++ b/.gitignore @@ -0,0 +1,20 @@ +node_modules +dist + +# Local dev database +docker-compose.dev.yml + +# Env +.env +.env.* +!.env.example +!.env.test + +# OS +.DS_Store +Thumbs.db + +# Editor/tool configs +.vscode/ +.claude/ +CLAUDE.md diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 94658f3..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "workbench.browser.enableChatTools": true -} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..2b6638e --- /dev/null +++ b/README.md @@ -0,0 +1,138 @@ +
+ Hack Club flag +

Beest

+

The NestJS + SvelteKit + PostgreSQL codebase powering Beest, a hackathon in the Netherlands

+
+ +--- + +# Beest + +The Beest codebase is what runs on https://beest.hackclub.com. That website is the You Ship We Ship platform allowing participants to sign in, create and share projects, recieve feedback and earn prizes through the shop. + + +## Architecture + +This is a monorepo with two applications: + +| Layer | Stack | Role | +|-------|-------|------| +| **`backend/`** | NestJS 11, TypeORM, PostgreSQL | Single source of truth - all auth, business logic, and data access | +| **`frontend/`** | SvelteKit 2, Svelte 5 | Thin proxy - renders UI, sets cookies, forwards requests to the backend | + + + +## Development Setup + +```bash +git clone https://github.com/hackclub/beest +cd beest + +# Start the database +docker compose -f docker-compose.dev.yml up -d + +# Backend +cd backend +npm install +cp .env.example .env # fill in credentials +npm run migration:run +npm run start:dev # runs on :3001 + +# Frontend (in a second terminal) +cd frontend +npm install +npm run dev # runs on :5173 +``` + +## Environment Variables + +### Backend (`backend/.env`) + +```bash +# Airtable +AIRTABLE_API_KEY= +AIRTABLE_BASE_ID= +AIRTABLE_TABLE_NAME= + +# Hack Club Auth OAuth +CLIENT_ID= +CLIENT_SECRET= +REDIRECT_URI=http://localhost:5173/oauth/callback + +# JWT & encryption +JWT_SECRET= +DB_ENCRYPTION_KEY= # 32-byte hex string for AES-256-GCM + +# Hackatime OAuth +HACKATIME_CLIENT_ID= +HACKATIME_CLIENT_SECRET= +HACKATIME_REDIRECT_URI=http://localhost:5173/auth/hackatime/callback +HACKATIME_BASE_URL=https://hackatime.hackclub.com + +# Database +DATABASE_URL=postgresql://postgres:password@localhost:5432/postgres + +# Slack +SLACK_BOT_TOKEN= +``` + +### Frontend (`frontend/.env`) + +```bash +BACKEND_URL=http://localhost:3001 +``` + +## Deployment + +### Docker Compose + +```bash +docker compose up --build +``` + +### Dockerfile (standalone) + +Both `backend/` and `frontend/` have their own multi-stage Dockerfiles (Node 22 Alpine). Point `DATABASE_URL` at your PostgreSQL instance and set all backend env vars. + +| Service | Internal Port | +|---------|---------------| +| Frontend | 3000 | +| Backend | 3001 | +| PostgreSQL | 5432 | + +## API + +All endpoints live under the backend at `/api`. Auth-protected routes require a `Bearer` JWT in the `Authorization` header. + +| Endpoint | Method | Auth | Purpose | +|----------|--------|------|---------| +| `/api/health` | GET | — | Health check | +| `/api/auth/start` | POST | — | Begin OAuth flow | +| `/api/auth/handle-callback` | POST | — | Complete OAuth, issue tokens | +| `/api/auth/refresh` | POST | — | Rotate refresh token | +| `/api/auth/me` | GET | JWT | Current user claims | +| `/api/auth/logout` | POST | — | Invalidate session | +| `/api/auth/rsvp` | POST | JWT | RSVP using authenticated session | +| `/api/auth/scope` | GET | JWT | Check user permissions | +| `/api/rsvp` | POST | — | Submit an RSVP | +| `/api/hackatime/start` | POST | JWT | Begin Hackatime OAuth | +| `/api/hackatime/callback` | POST | JWT | Complete Hackatime OAuth | +| `/api/hackatime/projects` | GET | JWT | User's Hackatime project names | +| `/api/projects` | GET | JWT | List user's projects | +| `/api/projects` | POST | JWT | Create a project | +| `/api/projects/:id` | PATCH | JWT | Update a project | +| `/api/projects/:id` | DELETE | JWT | Delete a project | +| `/api/projects/hours` | GET | JWT | Hackatime hours breakdown | +| `/api/leaderboard` | GET | JWT | Top 10 users by approved hours | +| `/api/onboarding/status` | GET | JWT | Onboarding step completion | +| `/api/onboarding/two-emails` | POST | JWT | Confirm different Slack email | +| `/api/onboarding/sticker-link` | GET | JWT | User's unique sticker form link | +| `/api/audit-log` | GET | JWT | User's audit log entries | +| `/api/admin/users` | GET | Admin | List all users | +| `/api/admin/users/:id` | GET | Admin | Get specific user | +| `/api/admin/users/:id/ban` | POST | Admin | Ban a user | +| `/api/admin/users/:id/perms` | PATCH | Admin | Update user permissions | + +--- + +Made with <3 by [euan](https://github.com/EDRipper) , give it a ⭐ \ No newline at end of file diff --git a/backend/.env.example b/backend/.env.example new file mode 100644 index 0000000..c57ec52 --- /dev/null +++ b/backend/.env.example @@ -0,0 +1,25 @@ +AIRTABLE_API_KEY= +AIRTABLE_BASE_ID= +AIRTABLE_TABLE_NAME= +PORT=3001 + +# Hack Club Auth +CLIENT_ID= +CLIENT_SECRET= +JWT_SECRET= +REDIRECT_URI=http://localhost:5173/oauth/callback + +# Hackatime OAuth +HACKATIME_CLIENT_ID= +HACKATIME_CLIENT_SECRET= +HACKATIME_REDIRECT_URI=http://localhost:5173/auth/hackatime/callback +HACKATIME_BASE_URL=https://hackatime.hackclub.com +HACKATIME_ADMIN_API_KEY= + +# Unified Airtable (read-only, for duplicate code URL checks) +UNIFIED_API_KEY= + +# Database (use container name in prod, public hostname in staging) +DATABASE_URL=postgresql://postgres:password@localhost:5432/postgres +# 32-byte hex key for AES-256-GCM column encryption (generate: node -e "console.log(require('crypto').randomBytes(32).toString('hex'))") +DB_ENCRYPTION_KEY= diff --git a/backend/.prettierrc b/backend/.prettierrc new file mode 100644 index 0000000..a20502b --- /dev/null +++ b/backend/.prettierrc @@ -0,0 +1,4 @@ +{ + "singleQuote": true, + "trailingComma": "all" +} diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..ad7fdcb --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,17 @@ +FROM node:22-alpine AS build +WORKDIR /app +COPY package*.json ./ +RUN npm ci +COPY . . +RUN npm run build + +FROM node:22-alpine +RUN apk add --no-cache curl +WORKDIR /app +COPY --from=build /app/dist ./dist +COPY --from=build /app/package*.json ./ +RUN npm ci --omit=dev +EXPOSE 3001 +HEALTHCHECK --interval=10s --timeout=3s --start-period=30s --retries=3 \ + CMD curl -f http://localhost:3001/api/health || exit 1 +CMD ["node", "dist/main"] diff --git a/backend/README.md b/backend/README.md new file mode 100644 index 0000000..8f0f65f --- /dev/null +++ b/backend/README.md @@ -0,0 +1,98 @@ +

+ Nest Logo +

+ +[circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456 +[circleci-url]: https://circleci.com/gh/nestjs/nest + +

A progressive Node.js framework for building efficient and scalable server-side applications.

+

+NPM Version +Package License +NPM Downloads +CircleCI +Discord +Backers on Open Collective +Sponsors on Open Collective + Donate us + Support us + Follow us on Twitter +

+ + +## Description + +[Nest](https://github.com/nestjs/nest) framework TypeScript starter repository. + +## Project setup + +```bash +$ npm install +``` + +## Compile and run the project + +```bash +# development +$ npm run start + +# watch mode +$ npm run start:dev + +# production mode +$ npm run start:prod +``` + +## Run tests + +```bash +# unit tests +$ npm run test + +# e2e tests +$ npm run test:e2e + +# test coverage +$ npm run test:cov +``` + +## Deployment + +When you're ready to deploy your NestJS application to production, there are some key steps you can take to ensure it runs as efficiently as possible. Check out the [deployment documentation](https://docs.nestjs.com/deployment) for more information. + +If you are looking for a cloud-based platform to deploy your NestJS application, check out [Mau](https://mau.nestjs.com), our official platform for deploying NestJS applications on AWS. Mau makes deployment straightforward and fast, requiring just a few simple steps: + +```bash +$ npm install -g @nestjs/mau +$ mau deploy +``` + +With Mau, you can deploy your application in just a few clicks, allowing you to focus on building features rather than managing infrastructure. + +## Resources + +Check out a few resources that may come in handy when working with NestJS: + +- Visit the [NestJS Documentation](https://docs.nestjs.com) to learn more about the framework. +- For questions and support, please visit our [Discord channel](https://discord.gg/G7Qnnhy). +- To dive deeper and get more hands-on experience, check out our official video [courses](https://courses.nestjs.com/). +- Deploy your application to AWS with the help of [NestJS Mau](https://mau.nestjs.com) in just a few clicks. +- Visualize your application graph and interact with the NestJS application in real-time using [NestJS Devtools](https://devtools.nestjs.com). +- Need help with your project (part-time to full-time)? Check out our official [enterprise support](https://enterprise.nestjs.com). +- To stay in the loop and get updates, follow us on [X](https://x.com/nestframework) and [LinkedIn](https://linkedin.com/company/nestjs). +- Looking for a job, or have a job to offer? Check out our official [Jobs board](https://jobs.nestjs.com). + +## Support + +Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support). + +## Stay in touch + +- Author - [Kamil Myśliwiec](https://twitter.com/kammysliwiec) +- Website - [https://nestjs.com](https://nestjs.com/) +- Twitter - [@nestframework](https://twitter.com/nestframework) + +## License + +Nest is [MIT licensed](https://github.com/nestjs/nest/blob/master/LICENSE). diff --git a/backend/eslint.config.mjs b/backend/eslint.config.mjs new file mode 100644 index 0000000..4e9f827 --- /dev/null +++ b/backend/eslint.config.mjs @@ -0,0 +1,35 @@ +// @ts-check +import eslint from '@eslint/js'; +import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended'; +import globals from 'globals'; +import tseslint from 'typescript-eslint'; + +export default tseslint.config( + { + ignores: ['eslint.config.mjs'], + }, + eslint.configs.recommended, + ...tseslint.configs.recommendedTypeChecked, + eslintPluginPrettierRecommended, + { + languageOptions: { + globals: { + ...globals.node, + ...globals.jest, + }, + sourceType: 'commonjs', + parserOptions: { + projectService: true, + tsconfigRootDir: import.meta.dirname, + }, + }, + }, + { + rules: { + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/no-floating-promises': 'warn', + '@typescript-eslint/no-unsafe-argument': 'warn', + "prettier/prettier": ["error", { endOfLine: "auto" }], + }, + }, +); diff --git a/backend/nest-cli.json b/backend/nest-cli.json new file mode 100644 index 0000000..f9aa683 --- /dev/null +++ b/backend/nest-cli.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://json.schemastore.org/nest-cli", + "collection": "@nestjs/schematics", + "sourceRoot": "src", + "compilerOptions": { + "deleteOutDir": true + } +} diff --git a/backend/package-lock.json b/backend/package-lock.json new file mode 100644 index 0000000..6cb059f --- /dev/null +++ b/backend/package-lock.json @@ -0,0 +1,10836 @@ +{ + "name": "backend", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "backend", + "version": "0.0.1", + "license": "UNLICENSED", + "dependencies": { + "@nestjs/common": "^11.0.1", + "@nestjs/config": "^4.0.3", + "@nestjs/core": "^11.0.1", + "@nestjs/jwt": "^11.0.2", + "@nestjs/platform-express": "^11.0.1", + "@nestjs/throttler": "^6.5.0", + "@nestjs/typeorm": "^11.0.0", + "helmet": "^8.1.0", + "jsonwebtoken": "^9.0.3", + "pg": "^8.20.0", + "reflect-metadata": "^0.2.2", + "rxjs": "^7.8.1", + "typeorm": "^0.3.28" + }, + "devDependencies": { + "@eslint/eslintrc": "^3.2.0", + "@eslint/js": "^9.18.0", + "@nestjs/cli": "^11.0.0", + "@nestjs/schematics": "^11.0.0", + "@nestjs/testing": "^11.0.1", + "@types/express": "^5.0.0", + "@types/jest": "^30.0.0", + "@types/jsonwebtoken": "^9.0.10", + "@types/node": "^22.10.7", + "@types/supertest": "^6.0.2", + "eslint": "^9.18.0", + "eslint-config-prettier": "^10.0.1", + "eslint-plugin-prettier": "^5.2.2", + "globals": "^16.0.0", + "jest": "^30.0.0", + "prettier": "^3.4.2", + "source-map-support": "^0.5.21", + "supertest": "^7.0.0", + "ts-jest": "^29.2.5", + "ts-loader": "^9.5.2", + "ts-node": "^10.9.2", + "tsconfig-paths": "^4.2.0", + "typescript": "^5.7.3", + "typescript-eslint": "^8.20.0" + } + }, + "node_modules/@angular-devkit/core": { + "version": "19.2.19", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-19.2.19.tgz", + "integrity": "sha512-JbLL+4IMLMBgjLZlnPG4lYDfz4zGrJ/s6Aoon321NJKuw1Kb1k5KpFu9dUY0BqLIe8xPQ2UJBpI+xXdK5MXMHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "8.17.1", + "ajv-formats": "3.0.1", + "jsonc-parser": "3.3.1", + "picomatch": "4.0.2", + "rxjs": "7.8.1", + "source-map": "0.7.4" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^4.0.0" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/core/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@angular-devkit/core/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/@angular-devkit/core/node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@angular-devkit/schematics": { + "version": "19.2.19", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-19.2.19.tgz", + "integrity": "sha512-J4Jarr0SohdrHcb40gTL4wGPCQ952IMWF1G/MSAQfBAPvA9ZKApYhpxcY7PmehVePve+ujpus1dGsJ7dPxz8Kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "19.2.19", + "jsonc-parser": "3.3.1", + "magic-string": "0.30.17", + "ora": "5.4.1", + "rxjs": "7.8.1" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular-devkit/schematics-cli": { + "version": "19.2.19", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics-cli/-/schematics-cli-19.2.19.tgz", + "integrity": "sha512-7q9UY6HK6sccL9F3cqGRUwKhM7b/XfD2YcVaZ2WD7VMaRlRm85v6mRjSrfKIAwxcQU0UK27kMc79NIIqaHjzxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "19.2.19", + "@angular-devkit/schematics": "19.2.19", + "@inquirer/prompts": "7.3.2", + "ansi-colors": "4.1.3", + "symbol-observable": "4.0.0", + "yargs-parser": "21.1.1" + }, + "bin": { + "schematics": "bin/schematics.js" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular-devkit/schematics-cli/node_modules/@inquirer/prompts": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.3.2.tgz", + "integrity": "sha512-G1ytyOoHh5BphmEBxSwALin3n1KGNYB6yImbICcRQdzXfOGbuJ9Jske/Of5Sebk339NSGGNfUshnzK8YWkTPsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/checkbox": "^4.1.2", + "@inquirer/confirm": "^5.1.6", + "@inquirer/editor": "^4.2.7", + "@inquirer/expand": "^4.0.9", + "@inquirer/input": "^4.1.6", + "@inquirer/number": "^3.0.9", + "@inquirer/password": "^4.0.9", + "@inquirer/rawlist": "^4.0.9", + "@inquirer/search": "^3.0.9", + "@inquirer/select": "^4.0.9" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/schematics/node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@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/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.28.6.tgz", + "integrity": "sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.28.6.tgz", + "integrity": "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.28.6.tgz", + "integrity": "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@borewit/text-codec": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@borewit/text-codec/-/text-codec-0.2.2.tgz", + "integrity": "sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@emnapi/core": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.1.tgz", + "integrity": "sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.0", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.1.tgz", + "integrity": "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.0.tgz", + "integrity": "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "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": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "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.5", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", + "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "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.1.1", + "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/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", + "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", + "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/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.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/@inquirer/ansi": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.2.tgz", + "integrity": "sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/checkbox": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.3.2.tgz", + "integrity": "sha512-VXukHf0RR1doGe6Sm4F0Em7SWYLTHSsbGfJdS9Ja2bX5/D5uwVOEjr07cncLROdBvmnvCATYEWlHqYmXv2IlQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/confirm": { + "version": "5.1.21", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.21.tgz", + "integrity": "sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core": { + "version": "10.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.3.2.tgz", + "integrity": "sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "cli-width": "^4.1.0", + "mute-stream": "^2.0.0", + "signal-exit": "^4.1.0", + "wrap-ansi": "^6.2.0", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/editor": { + "version": "4.2.23", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.23.tgz", + "integrity": "sha512-aLSROkEwirotxZ1pBaP8tugXRFCxW94gwrQLxXfrZsKkfjOYC1aRvAZuhpJOb5cu4IBTJdsCigUlf2iCOu4ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/external-editor": "^1.0.3", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/expand": { + "version": "4.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.23.tgz", + "integrity": "sha512-nRzdOyFYnpeYTTR2qFwEVmIWypzdAx/sIkCMeTNTcflFOovfqUk+HcFhQQVBftAh9gmGrpFj6QcGEqrDMDOiew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/external-editor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.3.tgz", + "integrity": "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chardet": "^2.1.1", + "iconv-lite": "^0.7.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/figures": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.15.tgz", + "integrity": "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/input": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.3.1.tgz", + "integrity": "sha512-kN0pAM4yPrLjJ1XJBjDxyfDduXOuQHrBB8aLDMueuwUGn+vNpF7Gq7TvyVxx8u4SHlFFj4trmj+a2cbpG4Jn1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/number": { + "version": "3.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.23.tgz", + "integrity": "sha512-5Smv0OK7K0KUzUfYUXDXQc9jrf8OHo4ktlEayFlelCjwMXz0299Y8OrI+lj7i4gCBY15UObk76q0QtxjzFcFcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/password": { + "version": "4.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.23.tgz", + "integrity": "sha512-zREJHjhT5vJBMZX/IUbyI9zVtVfOLiTO66MrF/3GFZYZ7T4YILW5MSkEYHceSii/KtRk+4i3RE7E1CUXA2jHcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/prompts": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.10.1.tgz", + "integrity": "sha512-Dx/y9bCQcXLI5ooQ5KyvA4FTgeo2jYj/7plWfV5Ak5wDPKQZgudKez2ixyfz7tKXzcJciTxqLeK7R9HItwiByg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/checkbox": "^4.3.2", + "@inquirer/confirm": "^5.1.21", + "@inquirer/editor": "^4.2.23", + "@inquirer/expand": "^4.0.23", + "@inquirer/input": "^4.3.1", + "@inquirer/number": "^3.0.23", + "@inquirer/password": "^4.0.23", + "@inquirer/rawlist": "^4.1.11", + "@inquirer/search": "^3.2.2", + "@inquirer/select": "^4.4.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/rawlist": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.1.11.tgz", + "integrity": "sha512-+LLQB8XGr3I5LZN/GuAHo+GpDJegQwuPARLChlMICNdwW7OwV2izlCSCxN6cqpL0sMXmbKbFcItJgdQq5EBXTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/search": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.2.2.tgz", + "integrity": "sha512-p2bvRfENXCZdWF/U2BXvnSI9h+tuA8iNqtUKb9UWbmLYCRQxd8WkvwWvYn+3NgYaNwdUkHytJMGG4MMLucI1kA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/select": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.4.2.tgz", + "integrity": "sha512-l4xMuJo55MAe+N7Qr4rX90vypFwCajSakx59qe/tMaC1aEHWLyw68wF4o0A4SLAY4E0nd+Vt+EyskeDIqu1M6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/type": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.10.tgz", + "integrity": "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/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/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/@istanbuljs/load-nyc-config/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==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@istanbuljs/load-nyc-config/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==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/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==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@istanbuljs/load-nyc-config/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==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-30.3.0.tgz", + "integrity": "sha512-PAwCvFJ4696XP2qZj+LAn1BWjZaJ6RjG6c7/lkMaUJnkyMS34ucuIsfqYvfskVNvUI27R/u4P1HMYFnlVXG/Ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.3.0", + "@types/node": "*", + "chalk": "^4.1.2", + "jest-message-util": "30.3.0", + "jest-util": "30.3.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/core": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-30.3.0.tgz", + "integrity": "sha512-U5mVPsBxLSO6xYbf+tgkymLx+iAhvZX43/xI1+ej2ZOPnPdkdO1CzDmFKh2mZBn2s4XZixszHeQnzp1gm/DIxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "30.3.0", + "@jest/pattern": "30.0.1", + "@jest/reporters": "30.3.0", + "@jest/test-result": "30.3.0", + "@jest/transform": "30.3.0", + "@jest/types": "30.3.0", + "@types/node": "*", + "ansi-escapes": "^4.3.2", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "exit-x": "^0.2.2", + "graceful-fs": "^4.2.11", + "jest-changed-files": "30.3.0", + "jest-config": "30.3.0", + "jest-haste-map": "30.3.0", + "jest-message-util": "30.3.0", + "jest-regex-util": "30.0.1", + "jest-resolve": "30.3.0", + "jest-resolve-dependencies": "30.3.0", + "jest-runner": "30.3.0", + "jest-runtime": "30.3.0", + "jest-snapshot": "30.3.0", + "jest-util": "30.3.0", + "jest-validate": "30.3.0", + "jest-watcher": "30.3.0", + "pretty-format": "30.3.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/diff-sequences": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.3.0.tgz", + "integrity": "sha512-cG51MVnLq1ecVUaQ3fr6YuuAOitHK1S4WUJHnsPFE/quQr33ADUx1FfrTCpMCRxvy0Yr9BThKpDjSlcTi91tMA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/environment": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.3.0.tgz", + "integrity": "sha512-SlLSF4Be735yQXyh2+mctBOzNDx5s5uLv88/j8Qn1wH679PDcwy67+YdADn8NJnGjzlXtN62asGH/T4vWOkfaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "30.3.0", + "@jest/types": "30.3.0", + "@types/node": "*", + "jest-mock": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-30.3.0.tgz", + "integrity": "sha512-76Nlh4xJxk2D/9URCn3wFi98d2hb19uWE1idLsTt2ywhvdOldbw3S570hBgn25P4ICUZ/cBjybrBex2g17IDbg==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "30.3.0", + "jest-snapshot": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.3.0.tgz", + "integrity": "sha512-j0+W5iQQ8hBh7tHZkTQv3q2Fh/M7Je72cIsYqC4OaktgtO7v1So9UTjp6uPBHIaB6beoF/RRsCgMJKvti0wADA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.3.0.tgz", + "integrity": "sha512-WUQDs8SOP9URStX1DzhD425CqbN/HxUYCTwVrT8sTVBfMvFqYt/s61EK5T05qnHu0po6RitXIvP9otZxYDzTGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.3.0", + "@sinonjs/fake-timers": "^15.0.0", + "@types/node": "*", + "jest-message-util": "30.3.0", + "jest-mock": "30.3.0", + "jest-util": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/get-type": { + "version": "30.1.0", + "resolved": "https://registry.npmjs.org/@jest/get-type/-/get-type-30.1.0.tgz", + "integrity": "sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-30.3.0.tgz", + "integrity": "sha512-+owLCBBdfpgL3HU+BD5etr1SvbXpSitJK0is1kiYjJxAAJggYMRQz5hSdd5pq1sSggfxPbw2ld71pt4x5wwViA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.3.0", + "@jest/expect": "30.3.0", + "@jest/types": "30.3.0", + "jest-mock": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/pattern": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.0.1.tgz", + "integrity": "sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-regex-util": "30.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-30.3.0.tgz", + "integrity": "sha512-a09z89S+PkQnL055bVj8+pe2Caed2PBOaczHcXCykW5ngxX9EWx/1uAwncxc/HiU0oZqfwseMjyhxgRjS49qPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "30.3.0", + "@jest/test-result": "30.3.0", + "@jest/transform": "30.3.0", + "@jest/types": "30.3.0", + "@jridgewell/trace-mapping": "^0.3.25", + "@types/node": "*", + "chalk": "^4.1.2", + "collect-v8-coverage": "^1.0.2", + "exit-x": "^0.2.2", + "glob": "^10.5.0", + "graceful-fs": "^4.2.11", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^5.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "30.3.0", + "jest-util": "30.3.0", + "jest-worker": "30.3.0", + "slash": "^3.0.0", + "string-length": "^4.0.2", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/reporters/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@jest/reporters/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@jest/reporters/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@jest/reporters/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@jest/reporters/node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/snapshot-utils": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/snapshot-utils/-/snapshot-utils-30.3.0.tgz", + "integrity": "sha512-ORbRN9sf5PP82v3FXNSwmO1OTDR2vzR2YTaR+E3VkSBZ8zadQE6IqYdYEeFH1NIkeB2HIGdF02dapb6K0Mj05g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.3.0", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "natural-compare": "^1.4.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-30.0.1.tgz", + "integrity": "sha512-MIRWMUUR3sdbP36oyNyhbThLHyJ2eEDClPCiHVbrYAe5g3CHRArIVpBw7cdSB5fr+ofSfIb2Tnsw8iEHL0PYQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "callsites": "^3.1.0", + "graceful-fs": "^4.2.11" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-30.3.0.tgz", + "integrity": "sha512-e/52nJGuD74AKTSe0P4y5wFRlaXP0qmrS17rqOMHeSwm278VyNyXE3gFO/4DTGF9w+65ra3lo3VKj0LBrzmgdQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "30.3.0", + "@jest/types": "30.3.0", + "@types/istanbul-lib-coverage": "^2.0.6", + "collect-v8-coverage": "^1.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-30.3.0.tgz", + "integrity": "sha512-dgbWy9b8QDlQeRZcv7LNF+/jFiiYHTKho1xirauZ7kVwY7avjFF6uTT0RqlgudB5OuIPagFdVtfFMosjVbk1eA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "30.3.0", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.3.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.3.0.tgz", + "integrity": "sha512-TLKY33fSLVd/lKB2YI1pH69ijyUblO/BQvCj566YvnwuzoTNr648iE0j22vRvVNk2HsPwByPxATg3MleS3gf5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", + "@jest/types": "30.3.0", + "@jridgewell/trace-mapping": "^0.3.25", + "babel-plugin-istanbul": "^7.0.1", + "chalk": "^4.1.2", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.3.0", + "jest-regex-util": "30.0.1", + "jest-util": "30.3.0", + "pirates": "^4.0.7", + "slash": "^3.0.0", + "write-file-atomic": "^5.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/types": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.3.0.tgz", + "integrity": "sha512-JHm87k7bA33hpBngtU8h6UBub/fqqA9uXfw+21j5Hmk7ooPHlboRNxHq0JcMtC+n8VJGP1mcfnD3Mk+XKe1oSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@lukeed/csprng": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@lukeed/csprng/-/csprng-1.1.0.tgz", + "integrity": "sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@napi-rs/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/@nestjs/cli": { + "version": "11.0.16", + "resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-11.0.16.tgz", + "integrity": "sha512-P0H+Vcjki6P5160E5QnMt3Q0X5FTg4PZkP99Ig4lm/4JWqfw32j3EXv3YBTJ2DmxLwOQ/IS9F7dzKpMAgzKTGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "19.2.19", + "@angular-devkit/schematics": "19.2.19", + "@angular-devkit/schematics-cli": "19.2.19", + "@inquirer/prompts": "7.10.1", + "@nestjs/schematics": "^11.0.1", + "ansis": "4.2.0", + "chokidar": "4.0.3", + "cli-table3": "0.6.5", + "commander": "4.1.1", + "fork-ts-checker-webpack-plugin": "9.1.0", + "glob": "13.0.0", + "node-emoji": "1.11.0", + "ora": "5.4.1", + "tsconfig-paths": "4.2.0", + "tsconfig-paths-webpack-plugin": "4.2.0", + "typescript": "5.9.3", + "webpack": "5.104.1", + "webpack-node-externals": "3.0.0" + }, + "bin": { + "nest": "bin/nest.js" + }, + "engines": { + "node": ">= 20.11" + }, + "peerDependencies": { + "@swc/cli": "^0.1.62 || ^0.3.0 || ^0.4.0 || ^0.5.0 || ^0.6.0 || ^0.7.0", + "@swc/core": "^1.3.62" + }, + "peerDependenciesMeta": { + "@swc/cli": { + "optional": true + }, + "@swc/core": { + "optional": true + } + } + }, + "node_modules/@nestjs/cli/node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@nestjs/cli/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==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/@nestjs/cli/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/@nestjs/cli/node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@nestjs/cli/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/@nestjs/cli/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/@nestjs/cli/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@nestjs/cli/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@nestjs/cli/node_modules/schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/@nestjs/cli/node_modules/webpack": { + "version": "5.104.1", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.104.1.tgz", + "integrity": "sha512-Qphch25abbMNtekmEGJmeRUhLDbe+QfiWTiqpKYkpCOWY64v9eyl+KRRLmqOFA2AvKPpc9DC6+u2n76tQLBoaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.8", + "@types/json-schema": "^7.0.15", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.15.0", + "acorn-import-phases": "^1.0.3", + "browserslist": "^4.28.1", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.17.4", + "es-module-lexer": "^2.0.0", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.3.1", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^4.3.3", + "tapable": "^2.3.0", + "terser-webpack-plugin": "^5.3.16", + "watchpack": "^2.4.4", + "webpack-sources": "^3.3.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/@nestjs/common": { + "version": "11.1.17", + "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.1.17.tgz", + "integrity": "sha512-hLODw5Abp8OQgA+mUO4tHou4krKgDtUcM9j5Ihxncst9XeyxYBTt2bwZm4e4EQr5E352S4Fyy6V3iFx9ggxKAg==", + "license": "MIT", + "dependencies": { + "file-type": "21.3.2", + "iterare": "1.2.1", + "load-esm": "1.0.3", + "tslib": "2.8.1", + "uid": "2.0.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "class-transformer": ">=0.4.1", + "class-validator": ">=0.13.2", + "reflect-metadata": "^0.1.12 || ^0.2.0", + "rxjs": "^7.1.0" + }, + "peerDependenciesMeta": { + "class-transformer": { + "optional": true + }, + "class-validator": { + "optional": true + } + } + }, + "node_modules/@nestjs/config": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@nestjs/config/-/config-4.0.3.tgz", + "integrity": "sha512-FQ3M3Ohqfl+nHAn5tp7++wUQw0f2nAk+SFKe8EpNRnIifPqvfJP6JQxPKtFLMOHbyer4X646prFG4zSRYEssQQ==", + "license": "MIT", + "dependencies": { + "dotenv": "17.2.3", + "dotenv-expand": "12.0.3", + "lodash": "4.17.23" + }, + "peerDependencies": { + "@nestjs/common": "^10.0.0 || ^11.0.0", + "rxjs": "^7.1.0" + } + }, + "node_modules/@nestjs/core": { + "version": "11.1.17", + "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-11.1.17.tgz", + "integrity": "sha512-lD5mAYekTTurF3vDaa8C2OKPnjiz4tsfxIc5XlcSUzOhkwWf6Ay3HKvt6FmvuWQam6uIIHX52Clg+e6tAvf/cg==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@nuxt/opencollective": "0.4.1", + "fast-safe-stringify": "2.1.1", + "iterare": "1.2.1", + "path-to-regexp": "8.3.0", + "tslib": "2.8.1", + "uid": "2.0.2" + }, + "engines": { + "node": ">= 20" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "@nestjs/common": "^11.0.0", + "@nestjs/microservices": "^11.0.0", + "@nestjs/platform-express": "^11.0.0", + "@nestjs/websockets": "^11.0.0", + "reflect-metadata": "^0.1.12 || ^0.2.0", + "rxjs": "^7.1.0" + }, + "peerDependenciesMeta": { + "@nestjs/microservices": { + "optional": true + }, + "@nestjs/platform-express": { + "optional": true + }, + "@nestjs/websockets": { + "optional": true + } + } + }, + "node_modules/@nestjs/jwt": { + "version": "11.0.2", + "resolved": "https://registry.npmjs.org/@nestjs/jwt/-/jwt-11.0.2.tgz", + "integrity": "sha512-rK8aE/3/Ma45gAWfCksAXUNbOoSOUudU0Kn3rT39htPF7wsYXtKfjALKeKKJbFrIWbLjsbqfXX5bIJNvgBugGA==", + "license": "MIT", + "dependencies": { + "@types/jsonwebtoken": "9.0.10", + "jsonwebtoken": "9.0.3" + }, + "peerDependencies": { + "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0" + } + }, + "node_modules/@nestjs/platform-express": { + "version": "11.1.17", + "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.1.17.tgz", + "integrity": "sha512-mAf4eOsSBsTOn/VbrUO1gsjW6dVh91qqXPMXun4dN8SnNjf7PTQagM9o8d6ab8ZBpNe6UdZftdrZoDetU+n4Qg==", + "license": "MIT", + "dependencies": { + "cors": "2.8.6", + "express": "5.2.1", + "multer": "2.1.1", + "path-to-regexp": "8.3.0", + "tslib": "2.8.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "@nestjs/common": "^11.0.0", + "@nestjs/core": "^11.0.0" + } + }, + "node_modules/@nestjs/schematics": { + "version": "11.0.9", + "resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-11.0.9.tgz", + "integrity": "sha512-0NfPbPlEaGwIT8/TCThxLzrlz3yzDNkfRNpbL7FiplKq3w4qXpJg0JYwqgMEJnLQZm3L/L/5XjoyfJHUO3qX9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "19.2.17", + "@angular-devkit/schematics": "19.2.17", + "comment-json": "4.4.1", + "jsonc-parser": "3.3.1", + "pluralize": "8.0.0" + }, + "peerDependencies": { + "typescript": ">=4.8.2" + } + }, + "node_modules/@nestjs/schematics/node_modules/@angular-devkit/core": { + "version": "19.2.17", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-19.2.17.tgz", + "integrity": "sha512-Ah008x2RJkd0F+NLKqIpA34/vUGwjlprRCkvddjDopAWRzYn6xCkz1Tqwuhn0nR1Dy47wTLKYD999TYl5ONOAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "8.17.1", + "ajv-formats": "3.0.1", + "jsonc-parser": "3.3.1", + "picomatch": "4.0.2", + "rxjs": "7.8.1", + "source-map": "0.7.4" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^4.0.0" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@nestjs/schematics/node_modules/@angular-devkit/schematics": { + "version": "19.2.17", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-19.2.17.tgz", + "integrity": "sha512-ADfbaBsrG8mBF6Mfs+crKA/2ykB8AJI50Cv9tKmZfwcUcyAdmTr+vVvhsBCfvUAEokigSsgqgpYxfkJVxhJYeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "19.2.17", + "jsonc-parser": "3.3.1", + "magic-string": "0.30.17", + "ora": "5.4.1", + "rxjs": "7.8.1" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@nestjs/schematics/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@nestjs/schematics/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/@nestjs/schematics/node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@nestjs/testing": { + "version": "11.1.17", + "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-11.1.17.tgz", + "integrity": "sha512-lNffw+z+2USewmw4W0tsK+Rq94A2N4PiHbcqoRUu5y8fnqxQeIWGHhjo5BFCqj7eivqJBhT7WdRydxVq4rAHzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "@nestjs/common": "^11.0.0", + "@nestjs/core": "^11.0.0", + "@nestjs/microservices": "^11.0.0", + "@nestjs/platform-express": "^11.0.0" + }, + "peerDependenciesMeta": { + "@nestjs/microservices": { + "optional": true + }, + "@nestjs/platform-express": { + "optional": true + } + } + }, + "node_modules/@nestjs/throttler": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@nestjs/throttler/-/throttler-6.5.0.tgz", + "integrity": "sha512-9j0ZRfH0QE1qyrj9JjIRDz5gQLPqq9yVC2nHsrosDVAfI5HHw08/aUAWx9DZLSdQf4HDkmhTTEGLrRFHENvchQ==", + "license": "MIT", + "peerDependencies": { + "@nestjs/common": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0", + "@nestjs/core": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0", + "reflect-metadata": "^0.1.13 || ^0.2.0" + } + }, + "node_modules/@nestjs/typeorm": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@nestjs/typeorm/-/typeorm-11.0.0.tgz", + "integrity": "sha512-SOeUQl70Lb2OfhGkvnh4KXWlsd+zA08RuuQgT7kKbzivngxzSo1Oc7Usu5VxCxACQC9wc2l9esOHILSJeK7rJA==", + "license": "MIT", + "peerDependencies": { + "@nestjs/common": "^10.0.0 || ^11.0.0", + "@nestjs/core": "^10.0.0 || ^11.0.0", + "reflect-metadata": "^0.1.13 || ^0.2.0", + "rxjs": "^7.2.0", + "typeorm": "^0.3.0" + } + }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@nuxt/opencollective": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@nuxt/opencollective/-/opencollective-0.4.1.tgz", + "integrity": "sha512-GXD3wy50qYbxCJ652bDrDzgMr3NFEkIS374+IgFQKkCvk9yiYcLvX2XDYr7UyQxf4wK0e+yqDYRubZ0DtOxnmQ==", + "license": "MIT", + "dependencies": { + "consola": "^3.2.3" + }, + "bin": { + "opencollective": "bin/opencollective.js" + }, + "engines": { + "node": "^14.18.0 || >=16.10.0", + "npm": ">=5.10.0" + } + }, + "node_modules/@paralleldrive/cuid2": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.3.1.tgz", + "integrity": "sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "^1.1.5" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@pkgr/core": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz", + "integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/pkgr" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.34.48", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz", + "integrity": "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-15.1.1.tgz", + "integrity": "sha512-cO5W33JgAPbOh07tvZjUOJ7oWhtaqGHiZw+11DPbyqh2kHTBc3eF/CjJDeQ4205RLQsX6rxCuYOroFQwl7JDRw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.1" + } + }, + "node_modules/@sqltools/formatter": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@sqltools/formatter/-/formatter-1.2.5.tgz", + "integrity": "sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw==", + "license": "MIT" + }, + "node_modules/@tokenizer/inflate": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.4.1.tgz", + "integrity": "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "token-types": "^6.1.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@tokenizer/token": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", + "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", + "license": "MIT" + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", + "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cookiejar": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz", + "integrity": "sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/eslint": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/express": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz", + "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "^2" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.1.tgz", + "integrity": "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "30.0.0", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-30.0.0.tgz", + "integrity": "sha512-XTYugzhuwqWjws0CVz8QpM36+T+Dz5mTEBKhNs/esGLnCIlGdRy+Dq78NRjd7ls7r8BC8ZRMOrKlkO1hU0JOwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^30.0.0", + "pretty-format": "^30.0.0" + } + }, + "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/jsonwebtoken": { + "version": "9.0.10", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", + "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==", + "license": "MIT", + "dependencies": { + "@types/ms": "*", + "@types/node": "*" + } + }, + "node_modules/@types/methods": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", + "integrity": "sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.19.15", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.15.tgz", + "integrity": "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/qs": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-JawvT8iBVWpzTrz3EGw9BTQFg3BQNmwERdKE22vlTxawwtbyUSlMppvZYKLZzB5zgACXdXxbD3m1bXaMqP/9ow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/superagent": { + "version": "8.1.9", + "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-8.1.9.tgz", + "integrity": "sha512-pTVjI73witn+9ILmoJdajHGW2jkSaOzhiFYF1Rd3EQ94kymLqB9PjD9ISg7WaALC7+dCHT0FGe9T2LktLq/3GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/cookiejar": "^2.1.5", + "@types/methods": "^1.1.4", + "@types/node": "*", + "form-data": "^4.0.0" + } + }, + "node_modules/@types/supertest": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/supertest/-/supertest-6.0.3.tgz", + "integrity": "sha512-8WzXq62EXFhJ7QsH3Ocb/iKQ/Ty9ZVWnVzoTKc9tyyFRRF3a74Tk2+TLFgaFFw364Ere+npzHKEJ6ga2LzIL7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/methods": "^1.1.4", + "@types/superagent": "^8.1.0" + } + }, + "node_modules/@types/yargs": { + "version": "17.0.35", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", + "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.57.2.tgz", + "integrity": "sha512-NZZgp0Fm2IkD+La5PR81sd+g+8oS6JwJje+aRWsDocxHkjyRw0J5L5ZTlN3LI1LlOcGL7ph3eaIUmTXMIjLk0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.57.2", + "@typescript-eslint/type-utils": "8.57.2", + "@typescript-eslint/utils": "8.57.2", + "@typescript-eslint/visitor-keys": "8.57.2", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.57.2", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.57.2.tgz", + "integrity": "sha512-30ScMRHIAD33JJQkgfGW1t8CURZtjc2JpTrq5n2HFhOefbAhb7ucc7xJwdWcrEtqUIYJ73Nybpsggii6GtAHjA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.57.2", + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/typescript-estree": "8.57.2", + "@typescript-eslint/visitor-keys": "8.57.2", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.57.2.tgz", + "integrity": "sha512-FuH0wipFywXRTHf+bTTjNyuNQQsQC3qh/dYzaM4I4W0jrCqjCVuUh99+xd9KamUfmCGPvbO8NDngo/vsnNVqgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.57.2", + "@typescript-eslint/types": "^8.57.2", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.57.2.tgz", + "integrity": "sha512-snZKH+W4WbWkrBqj4gUNRIGb/jipDW3qMqVJ4C9rzdFc+wLwruxk+2a5D+uoFcKPAqyqEnSb4l2ULuZf95eSkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/visitor-keys": "8.57.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.57.2.tgz", + "integrity": "sha512-3Lm5DSM+DCowsUOJC+YqHHnKEfFh5CoGkj5Z31NQSNF4l5wdOwqGn99wmwN/LImhfY3KJnmordBq/4+VDe2eKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.57.2.tgz", + "integrity": "sha512-Co6ZCShm6kIbAM/s+oYVpKFfW7LBc6FXoPXjTRQ449PPNBY8U0KZXuevz5IFuuUj2H9ss40atTaf9dlGLzbWZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/typescript-estree": "8.57.2", + "@typescript-eslint/utils": "8.57.2", + "debug": "^4.4.3", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.57.2.tgz", + "integrity": "sha512-/iZM6FnM4tnx9csuTxspMW4BOSegshwX5oBDznJ7S4WggL7Vczz5d2W11ecc4vRrQMQHXRSxzrCsyG5EsPPTbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.57.2.tgz", + "integrity": "sha512-2MKM+I6g8tJxfSmFKOnHv2t8Sk3T6rF20A1Puk0svLK+uVapDZB/4pfAeB7nE83uAZrU6OxW+HmOd5wHVdXwXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.57.2", + "@typescript-eslint/tsconfig-utils": "8.57.2", + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/visitor-keys": "8.57.2", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.57.2.tgz", + "integrity": "sha512-krRIbvPK1ju1WBKIefiX+bngPs+odIQUtR7kymzPfo1POVw3jlF+nLkmexdSSd4UCbDcQn+wMBATOOmpBbqgKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.57.2", + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/typescript-estree": "8.57.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.57.2.tgz", + "integrity": "sha512-zhahknjobV2FiD6Ee9iLbS7OV9zi10rG26odsQdfBO/hjSzUQbkIYgda+iNKK1zNiW2ey+Lf8MU5btN17V3dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.57.2", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@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", + "integrity": "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-android-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.11.1.tgz", + "integrity": "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.11.1.tgz", + "integrity": "sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.11.1.tgz", + "integrity": "sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-freebsd-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.11.1.tgz", + "integrity": "sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.11.1.tgz", + "integrity": "sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.11.1.tgz", + "integrity": "sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.11.1.tgz", + "integrity": "sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.11.1.tgz", + "integrity": "sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.11.1.tgz", + "integrity": "sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.11.1.tgz", + "integrity": "sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.11.1.tgz", + "integrity": "sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.11.1.tgz", + "integrity": "sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.11.1.tgz", + "integrity": "sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.11.1.tgz", + "integrity": "sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-wasm32-wasi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.11.1.tgz", + "integrity": "sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^0.2.11" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.11.1.tgz", + "integrity": "sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.11.1.tgz", + "integrity": "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-x64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz", + "integrity": "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "devOptional": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-import-phases": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", + "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + }, + "peerDependencies": { + "acorn": "^8.14.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ajv": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-formats/node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ansis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/ansis/-/ansis-4.2.0.tgz", + "integrity": "sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==", + "license": "ISC", + "engines": { + "node": ">=14" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/app-root-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/app-root-path/-/app-root-path-3.1.0.tgz", + "integrity": "sha512-biN3PwB2gUtjaYy/isrU3aNWI5w+fAfvHkSvCKeQGxhmYpwKFUxudR3Yya+KqVRHBmEDYh+/lTozYCFbmzX4nA==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", + "license": "MIT" + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/array-timsort": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array-timsort/-/array-timsort-1.0.3.tgz", + "integrity": "sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/babel-jest": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.3.0.tgz", + "integrity": "sha512-gRpauEU2KRrCox5Z296aeVHR4jQ98BCnu0IO332D/xpHNOsIH/bgSRk9k6GbKIbBw8vFeN6ctuu6tV8WOyVfYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/transform": "30.3.0", + "@types/babel__core": "^7.20.5", + "babel-plugin-istanbul": "^7.0.1", + "babel-preset-jest": "30.3.0", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.11.0 || ^8.0.0-0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-7.0.1.tgz", + "integrity": "sha512-D8Z6Qm8jCvVXtIRkBnqNHX0zJ37rQcFJ9u8WOS6tkYOsRdHBzypCstaxWiu5ZIlqQtviRYbgnRLSoCEvjqcqbA==", + "dev": true, + "license": "BSD-3-Clause", + "workspaces": [ + "test/babel-8" + ], + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-instrument": "^6.0.2", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-30.3.0.tgz", + "integrity": "sha512-+TRkByhsws6sfPjVaitzadk1I0F5sPvOVUH5tyTSzhePpsGIVrdeunHSw/C36QeocS95OOk8lunc4rlu5Anwsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/babel__core": "^7.20.5" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", + "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/babel-preset-jest": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-30.3.0.tgz", + "integrity": "sha512-6ZcUbWHC+dMz2vfzdNwi87Z1gQsLNK2uLuK1Q89R11xdvejcivlYYwDlEv0FHX3VwEXpbBQ9uufB/MUNpZGfhQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "30.3.0", + "babel-preset-current-node-syntax": "^1.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.11.0 || ^8.0.0-beta.1" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.10", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.10.tgz", + "integrity": "sha512-sUoJ3IMxx4AyRqO4MLeHlnGDkyXRoUG0/AI9fjK+vS72ekpV0yWVY7O0BVjmBcRtkNcsAO2QDZ4tdKKGoI6YaQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-json-stable-stringify": "2.x" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001781", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001781.tgz", + "integrity": "sha512-RdwNCyMsNBftLjW6w01z8bKEvT6e/5tpPVEgtn22TiLGlstHOVecsX2KHFkD5e/vRnIE4EGzpuIODb3mtswtkw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/chardet": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.1.tgz", + "integrity": "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/chrome-trace-event": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0" + } + }, + "node_modules/ci-info": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", + "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.2.0.tgz", + "integrity": "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-table3": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.5.tgz", + "integrity": "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "string-width": "^4.2.0" + }, + "engines": { + "node": "10.* || >= 12.*" + }, + "optionalDependencies": { + "@colors/colors": "1.5.0" + } + }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz", + "integrity": "sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==", + "dev": true, + "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==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/comment-json": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/comment-json/-/comment-json-4.4.1.tgz", + "integrity": "sha512-r1To31BQD5060QdkC+Iheai7gHwoSZobzunqkf2/kQ6xIAfJyrKNAFUwdKvkK7Qgu7pVTKQEa7ok7Ed3ycAJgg==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-timsort": "^1.0.3", + "core-util-is": "^1.0.3", + "esprima": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/component-emitter": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", + "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "engines": [ + "node >= 6.0" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/content-disposition": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", + "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cookiejar": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", + "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", + "dev": true, + "license": "MIT" + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cosmiconfig": { + "version": "8.3.6", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", + "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0", + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/dayjs": { + "version": "1.11.20", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.20.tgz", + "integrity": "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/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", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone": "^1.0.2" + }, + "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", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/dezalgo": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", + "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", + "dev": true, + "license": "ISC", + "dependencies": { + "asap": "^2.0.0", + "wrappy": "1" + } + }, + "node_modules/diff": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", + "devOptional": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dotenv": { + "version": "17.2.3", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz", + "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dotenv-expand": { + "version": "12.0.3", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-12.0.3.tgz", + "integrity": "sha512-uc47g4b+4k/M/SeaW1y4OApx+mtLWl92l5LMPP0GNXctZqELk+YGgOPIIC5elYmUH4OuoK3JLhuRUYegeySiFA==", + "license": "BSD-2-Clause", + "dependencies": { + "dotenv": "^16.4.5" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dotenv-expand/node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "license": "MIT" + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.325", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.325.tgz", + "integrity": "sha512-PwfIw7WQSt3xX7yOf5OE/unLzsK9CaN2f/FvV3WjPR1Knoc1T9vePRVV4W1EM301JzzysK51K7FNKcusCr0zYA==", + "dev": true, + "license": "ISC" + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.20.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz", + "integrity": "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", + "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", + "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@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.5", + "@eslint/js": "9.39.4", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "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": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-config-prettier": { + "version": "10.1.8", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", + "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "funding": { + "url": "https://opencollective.com/eslint-config-prettier" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-prettier": { + "version": "5.5.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.5.tgz", + "integrity": "sha512-hscXkbqUZ2sPithAuLm5MXL+Wph+U7wHngPBv9OMWwlP8iaflyxpjTYZkmdgB4/vPIhemRlBEoLrH7UC1n7aUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "prettier-linter-helpers": "^1.0.1", + "synckit": "^0.11.12" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-plugin-prettier" + }, + "peerDependencies": { + "@types/eslint": ">=8.0.0", + "eslint": ">=8.0.0", + "eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0", + "prettier": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@types/eslint": { + "optional": true + }, + "eslint-config-prettier": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "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": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "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": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/execa/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==", + "dev": true, + "license": "ISC" + }, + "node_modules/exit-x": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/exit-x/-/exit-x-0.2.2.tgz", + "integrity": "sha512-+I6B/IkJc1o/2tiURyz/ivu/O0nKNEArIUB5O7zBrlDVJr22SCLH3xTeEry428LvFhRzIA1g8izguxJ/gbNcVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-30.3.0.tgz", + "integrity": "sha512-1zQrciTiQfRdo7qJM1uG4navm8DayFa2TgCSRlzUyNkhcJ6XUZF3hjnpkyr3VhAqPH7i/9GkG7Tv5abz6fqz0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "30.3.0", + "@jest/get-type": "30.1.0", + "jest-matcher-utils": "30.3.0", + "jest-message-util": "30.3.0", + "jest-mock": "30.3.0", + "jest-util": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "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-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "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": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/file-type": { + "version": "21.3.2", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-21.3.2.tgz", + "integrity": "sha512-DLkUvGwep3poOV2wpzbHCOnSKGk1LzyXTv+aHFgN2VFl96wnp8YA9YjO2qPzg5PuL8q/SW9Pdi6WTkYOIh995w==", + "license": "MIT", + "dependencies": { + "@tokenizer/inflate": "^0.4.1", + "strtok3": "^10.3.4", + "token-types": "^6.1.1", + "uint8array-extras": "^1.4.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "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.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fork-ts-checker-webpack-plugin": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-9.1.0.tgz", + "integrity": "sha512-mpafl89VFPJmhnJ1ssH+8wmM2b50n+Rew5x42NeI2U78aRWgtkEtGmctp7iT16UjquJTjorEmIfESj3DxdW84Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.16.7", + "chalk": "^4.1.2", + "chokidar": "^4.0.1", + "cosmiconfig": "^8.2.0", + "deepmerge": "^4.2.2", + "fs-extra": "^10.0.0", + "memfs": "^3.4.1", + "minimatch": "^3.0.4", + "node-abort-controller": "^3.0.1", + "schema-utils": "^3.1.1", + "semver": "^7.3.5", + "tapable": "^2.2.1" + }, + "engines": { + "node": ">=14.21.3" + }, + "peerDependencies": { + "typescript": ">3.6.0", + "webpack": "^5.11.0" + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/form-data/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/form-data/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/formidable": { + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.4.tgz", + "integrity": "sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@paralleldrive/cuid2": "^2.2.2", + "dezalgo": "^1.0.4", + "once": "^1.4.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "url": "https://ko-fi.com/tunnckoCore/commissions" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/fs-monkey": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.1.0.tgz", + "integrity": "sha512-QMUezzXWII9EV5aTFXW1UBVUO77wYPpjqIF8/AviUCThNeSYZykpoTixUeaNNBwmCev0AMDWMAni+f8Hxb1IFw==", + "dev": true, + "license": "Unlicense" + }, + "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/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/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-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.0.tgz", + "integrity": "sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.1.1", + "minipass": "^7.1.2", + "path-scurry": "^2.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "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", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/glob/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/globals": { + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz", + "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/handlebars": { + "version": "4.7.8", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", + "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, + "node_modules/handlebars/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==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/helmet": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/helmet/-/helmet-8.1.0.tgz", + "integrity": "sha512-jOiHyAZsmnr8LqoPGmCjYAaiuWwjAPLgY8ZX2XrmHawt99/u1y6RgrZMTeoPfpUbV96HOalYgz1qzkRbw54Pmg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/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==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/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" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "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/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "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", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/iterare": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/iterare/-/iterare-1.2.1.tgz", + "integrity": "sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q==", + "license": "ISC", + "engines": { + "node": ">=6" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jest": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-30.3.0.tgz", + "integrity": "sha512-AkXIIFcaazymvey2i/+F94XRnM6TsVLZDhBMLsd1Sf/W0wzsvvpjeyUrCZD6HGG4SDYPgDJDBKeiJTBb10WzMg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "30.3.0", + "@jest/types": "30.3.0", + "import-local": "^3.2.0", + "jest-cli": "30.3.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-30.3.0.tgz", + "integrity": "sha512-B/7Cny6cV5At6M25EWDgf9S617lHivamL8vl6KEpJqkStauzcG4e+WPfDgMMF+H4FVH4A2PLRyvgDJan4441QA==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^5.1.1", + "jest-util": "30.3.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-circus": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-30.3.0.tgz", + "integrity": "sha512-PyXq5szeSfR/4f1lYqCmmQjh0vqDkURUYi9N6whnHjlRz4IUQfMcXkGLeEoiJtxtyPqgUaUUfyQlApXWBSN1RA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.3.0", + "@jest/expect": "30.3.0", + "@jest/test-result": "30.3.0", + "@jest/types": "30.3.0", + "@types/node": "*", + "chalk": "^4.1.2", + "co": "^4.6.0", + "dedent": "^1.6.0", + "is-generator-fn": "^2.1.0", + "jest-each": "30.3.0", + "jest-matcher-utils": "30.3.0", + "jest-message-util": "30.3.0", + "jest-runtime": "30.3.0", + "jest-snapshot": "30.3.0", + "jest-util": "30.3.0", + "p-limit": "^3.1.0", + "pretty-format": "30.3.0", + "pure-rand": "^7.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-cli": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-30.3.0.tgz", + "integrity": "sha512-l6Tqx+j1fDXJEW5bqYykDQQ7mQg+9mhWXtnj+tQZrTWYHyHoi6Be8HPumDSA+UiX2/2buEgjA58iJzdj146uCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "30.3.0", + "@jest/test-result": "30.3.0", + "@jest/types": "30.3.0", + "chalk": "^4.1.2", + "exit-x": "^0.2.2", + "import-local": "^3.2.0", + "jest-config": "30.3.0", + "jest-util": "30.3.0", + "jest-validate": "30.3.0", + "yargs": "^17.7.2" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-30.3.0.tgz", + "integrity": "sha512-WPMAkMAtNDY9P/oKObtsRG/6KTrhtgPJoBTmk20uDn4Uy6/3EJnnaZJre/FMT1KVRx8cve1r7/FlMIOfRVWL4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", + "@jest/get-type": "30.1.0", + "@jest/pattern": "30.0.1", + "@jest/test-sequencer": "30.3.0", + "@jest/types": "30.3.0", + "babel-jest": "30.3.0", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "deepmerge": "^4.3.1", + "glob": "^10.5.0", + "graceful-fs": "^4.2.11", + "jest-circus": "30.3.0", + "jest-docblock": "30.2.0", + "jest-environment-node": "30.3.0", + "jest-regex-util": "30.0.1", + "jest-resolve": "30.3.0", + "jest-runner": "30.3.0", + "jest-util": "30.3.0", + "jest-validate": "30.3.0", + "parse-json": "^5.2.0", + "pretty-format": "30.3.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "esbuild-register": ">=3.4.0", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "esbuild-register": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-config/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/jest-config/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jest-config/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/jest-config/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jest-config/node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jest-diff": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.3.0.tgz", + "integrity": "sha512-n3q4PDQjS4LrKxfWB3Z5KNk1XjXtZTBwQp71OP0Jo03Z6V60x++K5L8k6ZrW8MY8pOFylZvHM0zsjS1RqlHJZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/diff-sequences": "30.3.0", + "@jest/get-type": "30.1.0", + "chalk": "^4.1.2", + "pretty-format": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-30.2.0.tgz", + "integrity": "sha512-tR/FFgZKS1CXluOQzZvNH3+0z9jXr3ldGSD8bhyuxvlVUwbeLOGynkunvlTMxchC5urrKndYiwCFC0DLVjpOCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-newline": "^3.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-each": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-30.3.0.tgz", + "integrity": "sha512-V8eMndg/aZ+3LnCJgSm13IxS5XSBM22QSZc9BtPK8Dek6pm+hfUNfwBdvsB3d342bo1q7wnSkC38zjX259qZNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "@jest/types": "30.3.0", + "chalk": "^4.1.2", + "jest-util": "30.3.0", + "pretty-format": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-30.3.0.tgz", + "integrity": "sha512-4i6HItw/JSiJVsC5q0hnKIe/hbYfZLVG9YJ/0pU9Hz2n/9qZe3Rhn5s5CUZA5ORZlcdT/vmAXRMyONXJwPrmYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.3.0", + "@jest/fake-timers": "30.3.0", + "@jest/types": "30.3.0", + "@types/node": "*", + "jest-mock": "30.3.0", + "jest-util": "30.3.0", + "jest-validate": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.3.0.tgz", + "integrity": "sha512-mMi2oqG4KRU0R9QEtscl87JzMXfUhbKaFqOxmjb2CKcbHcUGFrJCBWHmnTiUqi6JcnzoBlO4rWfpdl2k/RfLCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.3.0", + "@types/node": "*", + "anymatch": "^3.1.3", + "fb-watchman": "^2.0.2", + "graceful-fs": "^4.2.11", + "jest-regex-util": "30.0.1", + "jest-util": "30.3.0", + "jest-worker": "30.3.0", + "picomatch": "^4.0.3", + "walker": "^1.0.8" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.3" + } + }, + "node_modules/jest-haste-map/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/jest-leak-detector": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-30.3.0.tgz", + "integrity": "sha512-cuKmUUGIjfXZAiGJ7TbEMx0bcqNdPPI6P1V+7aF+m/FUJqFDxkFR4JqkTu8ZOiU5AaX/x0hZ20KaaIPXQzbMGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "pretty-format": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.3.0.tgz", + "integrity": "sha512-HEtc9uFQgaUHkC7nLSlQL3Tph4Pjxt/yiPvkIrrDCt9jhoLIgxaubo1G+CFOnmHYMxHwwdaSN7mkIFs6ZK8OhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "chalk": "^4.1.2", + "jest-diff": "30.3.0", + "pretty-format": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.3.0.tgz", + "integrity": "sha512-Z/j4Bo+4ySJ+JPJN3b2Qbl9hDq3VrXmnjjGEWD/x0BCXeOXPTV1iZYYzl2X8c1MaCOL+ewMyNBcm88sboE6YWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@jest/types": "30.3.0", + "@types/stack-utils": "^2.0.3", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.3", + "pretty-format": "30.3.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-message-util/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/jest-mock": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.3.0.tgz", + "integrity": "sha512-OTzICK8CpE+t4ndhKrwlIdbM6Pn8j00lvmSmq5ejiO+KxukbLjgOflKWMn3KE34EZdQm5RqTuKj+5RIEniYhog==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.3.0", + "@types/node": "*", + "jest-util": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz", + "integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-30.3.0.tgz", + "integrity": "sha512-NRtTAHQlpd15F9rUR36jqwelbrDV/dY4vzNte3S2kxCKUJRYNd5/6nTSbYiak1VX5g8IoFF23Uj5TURkUW8O5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.3.0", + "jest-pnp-resolver": "^1.2.3", + "jest-util": "30.3.0", + "jest-validate": "30.3.0", + "slash": "^3.0.0", + "unrs-resolver": "^1.7.11" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-30.3.0.tgz", + "integrity": "sha512-9ev8s3YN6Hsyz9LV75XUwkCVFlwPbaFn6Wp75qnI0wzAINYWY8Fb3+6y59Rwd3QaS3kKXffHXsZMziMavfz/nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-regex-util": "30.0.1", + "jest-snapshot": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-runner": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-30.3.0.tgz", + "integrity": "sha512-gDv6C9LGKWDPLia9TSzZwf4h3kMQCqyTpq+95PODnTRDO0g9os48XIYYkS6D236vjpBir2fF63YmJFtqkS5Duw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "30.3.0", + "@jest/environment": "30.3.0", + "@jest/test-result": "30.3.0", + "@jest/transform": "30.3.0", + "@jest/types": "30.3.0", + "@types/node": "*", + "chalk": "^4.1.2", + "emittery": "^0.13.1", + "exit-x": "^0.2.2", + "graceful-fs": "^4.2.11", + "jest-docblock": "30.2.0", + "jest-environment-node": "30.3.0", + "jest-haste-map": "30.3.0", + "jest-leak-detector": "30.3.0", + "jest-message-util": "30.3.0", + "jest-resolve": "30.3.0", + "jest-runtime": "30.3.0", + "jest-util": "30.3.0", + "jest-watcher": "30.3.0", + "jest-worker": "30.3.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-runner/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==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/jest-runner/node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/jest-runtime": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-30.3.0.tgz", + "integrity": "sha512-CgC+hIBJbuh78HEffkhNKcbXAytQViplcl8xupqeIWyKQF50kCQA8J7GeJCkjisC6hpnC9Muf8jV5RdtdFbGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.3.0", + "@jest/fake-timers": "30.3.0", + "@jest/globals": "30.3.0", + "@jest/source-map": "30.0.1", + "@jest/test-result": "30.3.0", + "@jest/transform": "30.3.0", + "@jest/types": "30.3.0", + "@types/node": "*", + "chalk": "^4.1.2", + "cjs-module-lexer": "^2.1.0", + "collect-v8-coverage": "^1.0.2", + "glob": "^10.5.0", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.3.0", + "jest-message-util": "30.3.0", + "jest-mock": "30.3.0", + "jest-regex-util": "30.0.1", + "jest-resolve": "30.3.0", + "jest-snapshot": "30.3.0", + "jest-util": "30.3.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-runtime/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/jest-runtime/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jest-runtime/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/jest-runtime/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jest-runtime/node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jest-snapshot": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-30.3.0.tgz", + "integrity": "sha512-f14c7atpb4O2DeNhwcvS810Y63wEn8O1HqK/luJ4F6M4NjvxmAKQwBUWjbExUtMxWJQ0wVgmCKymeJK6NZMnfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", + "@babel/generator": "^7.27.5", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/plugin-syntax-typescript": "^7.27.1", + "@babel/types": "^7.27.3", + "@jest/expect-utils": "30.3.0", + "@jest/get-type": "30.1.0", + "@jest/snapshot-utils": "30.3.0", + "@jest/transform": "30.3.0", + "@jest/types": "30.3.0", + "babel-preset-current-node-syntax": "^1.2.0", + "chalk": "^4.1.2", + "expect": "30.3.0", + "graceful-fs": "^4.2.11", + "jest-diff": "30.3.0", + "jest-matcher-utils": "30.3.0", + "jest-message-util": "30.3.0", + "jest-util": "30.3.0", + "pretty-format": "30.3.0", + "semver": "^7.7.2", + "synckit": "^0.11.8" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-util": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.3.0.tgz", + "integrity": "sha512-/jZDa00a3Sz7rdyu55NLrQCIrbyIkbBxareejQI315f/i8HjYN+ZWsDLLpoQSiUIEIyZF/R8fDg3BmB8AtHttg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.3.0", + "@types/node": "*", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.3" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-util/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/jest-validate": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-30.3.0.tgz", + "integrity": "sha512-I/xzC8h5G+SHCb2P2gWkJYrNiTbeL47KvKeW5EzplkyxzBRBw1ssSHlI/jXec0ukH2q7x2zAWQm7015iusg62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "@jest/types": "30.3.0", + "camelcase": "^6.3.0", + "chalk": "^4.1.2", + "leven": "^3.1.0", + "pretty-format": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watcher": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-30.3.0.tgz", + "integrity": "sha512-PJ1d9ThtTR8aMiBWUdcownq9mDdLXsQzJayTk4kmaBRHKvwNQn+ANveuhEBUyNI2hR1TVhvQ8D5kHubbzBHR/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "30.3.0", + "@jest/types": "30.3.0", + "@types/node": "*", + "ansi-escapes": "^4.3.2", + "chalk": "^4.1.2", + "emittery": "^0.13.1", + "jest-util": "30.3.0", + "string-length": "^4.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-worker": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.3.0.tgz", + "integrity": "sha512-DrCKkaQwHexjRUFTmPzs7sHQe0TSj9nvDALKGdwmK5mW9v7j90BudWirKAJHt3QQ9Dhrg1F7DogPzhChppkJpQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@ungap/structured-clone": "^1.3.0", + "jest-util": "30.3.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.1.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "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==", + "dev": true, + "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", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/load-esm": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/load-esm/-/load-esm-1.0.3.tgz", + "integrity": "sha512-v5xlu8eHD1+6r8EHTg6hfmO97LN8ugKtiXcy5e6oN72iD2r6u0RPfLl6fxM+7Wnh2ZRq15o0russMst44WauPA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + }, + { + "type": "buymeacoffee", + "url": "https://buymeacoffee.com/borewit" + } + ], + "license": "MIT", + "engines": { + "node": ">=13.2.0" + } + }, + "node_modules/loader-runner": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz", + "integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.11.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "license": "MIT" + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/magic-string": { + "version": "0.30.17", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", + "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "devOptional": true, + "license": "ISC" + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/memfs": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.5.3.tgz", + "integrity": "sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==", + "dev": true, + "license": "Unlicense", + "dependencies": { + "fs-monkey": "^1.0.4" + }, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "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": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "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" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/multer": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/multer/-/multer-2.1.1.tgz", + "integrity": "sha512-mo+QTzKlx8R7E5ylSXxWzGoXoZbOsRMpyitcht8By2KHvMbf3tjwosZ/Mu/XYU6UuJ3VZnODIrak5ZrPiPyB6A==", + "license": "MIT", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.6.0", + "concat-stream": "^2.0.0", + "type-is": "^1.6.18" + }, + "engines": { + "node": ">= 10.16.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/multer/node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/multer/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/multer/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/multer/node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mute-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", + "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/napi-postinstall": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz", + "integrity": "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==", + "dev": true, + "license": "MIT", + "bin": { + "napi-postinstall": "lib/cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/napi-postinstall" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-abort-controller": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz", + "integrity": "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-emoji": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.11.0.tgz", + "integrity": "sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash": "^4.17.21" + } + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.36", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz", + "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/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==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.0" + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "11.2.7", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz", + "integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/path-to-regexp": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pg": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.20.0.tgz", + "integrity": "sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA==", + "license": "MIT", + "dependencies": { + "pg-connection-string": "^2.12.0", + "pg-pool": "^3.13.0", + "pg-protocol": "^1.13.0", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.3.0" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.3.0.tgz", + "integrity": "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==", + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.12.0.tgz", + "integrity": "sha512-U7qg+bpswf3Cs5xLzRqbXbQl85ng0mfSV/J0nnA31MCLgvEaAo7CIhmeyrmJpOr7o+zm0rXK+hNnT5l9RHkCkQ==", + "license": "MIT" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.13.0.tgz", + "integrity": "sha512-gB+R+Xud1gLFuRD/QgOIgGOBE2KCQPaPwkzBBGC9oG69pHTkhQeIuejVIk3/cnDyX39av2AxomQiyPT13WKHQA==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.13.0.tgz", + "integrity": "sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w==", + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "license": "MIT", + "dependencies": { + "split2": "^4.1.0" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/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==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/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==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/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==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/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==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pluralize": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", + "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "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", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz", + "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", + "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.1.tgz", + "integrity": "sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/pretty-format": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.3.0.tgz", + "integrity": "sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/pure-rand": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-7.0.1.tgz", + "integrity": "sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/qs": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/reflect-metadata": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", + "license": "Apache-2.0" + }, + "node_modules/require-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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-cwd/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/restore-cursor/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==", + "dev": true, + "license": "ISC" + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/sha.js": { + "version": "2.4.12", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.12.tgz", + "integrity": "sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==", + "license": "(MIT AND BSD-3-Clause)", + "dependencies": { + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.0" + }, + "bin": { + "sha.js": "bin.js" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 8" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/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==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/sql-highlight": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/sql-highlight/-/sql-highlight-6.1.0.tgz", + "integrity": "sha512-ed7OK4e9ywpE7pgRMkMQmZDPKSVdm0oX5IEtZiKnFucSF0zu6c80GZBe38UqHuVhTWJ9xsKgSMjCG2bml86KvA==", + "funding": [ + "https://github.com/scriptcoded/sql-highlight?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/scriptcoded" + } + ], + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "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-cjs": { + "name": "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/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strtok3": { + "version": "10.3.5", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.3.5.tgz", + "integrity": "sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA==", + "license": "MIT", + "dependencies": { + "@tokenizer/token": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/superagent": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-10.3.0.tgz", + "integrity": "sha512-B+4Ik7ROgVKrQsXTV0Jwp2u+PXYLSlqtDAhYnkkD+zn3yg8s/zjA2MeGayPoY/KICrbitwneDHrjSotxKL+0XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "component-emitter": "^1.3.1", + "cookiejar": "^2.1.4", + "debug": "^4.3.7", + "fast-safe-stringify": "^2.1.1", + "form-data": "^4.0.5", + "formidable": "^3.5.4", + "methods": "^1.1.2", + "mime": "2.6.0", + "qs": "^6.14.1" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/supertest": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/supertest/-/supertest-7.2.2.tgz", + "integrity": "sha512-oK8WG9diS3DlhdUkcFn4tkNIiIbBx9lI2ClF8K+b2/m8Eyv47LSawxUzZQSNKUrVb2KsqeTDCcjAAVPYaSLVTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cookie-signature": "^1.2.2", + "methods": "^1.1.2", + "superagent": "^10.3.0" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/symbol-observable": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz", + "integrity": "sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/synckit": { + "version": "0.11.12", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.12.tgz", + "integrity": "sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pkgr/core": "^0.2.9" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/synckit" + } + }, + "node_modules/tapable": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.2.tgz", + "integrity": "sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/terser": { + "version": "5.46.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.1.tgz", + "integrity": "sha512-vzCjQO/rgUuK9sf8VJZvjqiqiHFaZLnOiimmUuOKODxWL8mm/xua7viT7aqX7dgPY60otQjUotzFMmCB4VdmqQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.4.0.tgz", + "integrity": "sha512-Bn5vxm48flOIfkdl5CaD2+1CiUVbonWQ3KQPyP7/EuIl9Gbzq/gQFOzaMFUEgVjB1396tcK0SG8XcNJ/2kDH8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.0", + "terser": "^5.31.1" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/terser-webpack-plugin/node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/terser-webpack-plugin/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==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/terser-webpack-plugin/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/terser-webpack-plugin/node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/terser-webpack-plugin/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/terser-webpack-plugin/node_modules/schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/terser-webpack-plugin/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "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/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/to-buffer": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.2.tgz", + "integrity": "sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==", + "license": "MIT", + "dependencies": { + "isarray": "^2.0.5", + "safe-buffer": "^5.2.1", + "typed-array-buffer": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/token-types": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.1.2.tgz", + "integrity": "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==", + "license": "MIT", + "dependencies": { + "@borewit/text-codec": "^0.2.1", + "@tokenizer/token": "^0.3.0", + "ieee754": "^1.2.1" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/ts-jest": { + "version": "29.4.6", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.6.tgz", + "integrity": "sha512-fSpWtOO/1AjSNQguk43hb/JCo16oJDnMJf3CdEGNkqsEX3t0KX96xvyX1D7PfLCpVoKu4MfVrqUkFyblYoY4lA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bs-logger": "^0.2.6", + "fast-json-stable-stringify": "^2.1.0", + "handlebars": "^4.7.8", + "json5": "^2.2.3", + "lodash.memoize": "^4.1.2", + "make-error": "^1.3.6", + "semver": "^7.7.3", + "type-fest": "^4.41.0", + "yargs-parser": "^21.1.1" + }, + "bin": { + "ts-jest": "cli.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.0 <8", + "@jest/transform": "^29.0.0 || ^30.0.0", + "@jest/types": "^29.0.0 || ^30.0.0", + "babel-jest": "^29.0.0 || ^30.0.0", + "jest": "^29.0.0 || ^30.0.0", + "jest-util": "^29.0.0 || ^30.0.0", + "typescript": ">=4.3 <6" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@jest/transform": { + "optional": true + }, + "@jest/types": { + "optional": true + }, + "babel-jest": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jest-util": { + "optional": true + } + } + }, + "node_modules/ts-jest/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ts-loader": { + "version": "9.5.4", + "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.5.4.tgz", + "integrity": "sha512-nCz0rEwunlTZiy6rXFByQU1kVVpCIgUpc/psFiKVrUwrizdnIbRFu8w7bxhUF0X613DYwT4XzrZHpVyMe758hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "enhanced-resolve": "^5.0.0", + "micromatch": "^4.0.0", + "semver": "^7.3.4", + "source-map": "^0.7.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "typescript": "*", + "webpack": "^5.0.0" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/tsconfig-paths": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", + "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "json5": "^2.2.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tsconfig-paths-webpack-plugin": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths-webpack-plugin/-/tsconfig-paths-webpack-plugin-4.2.0.tgz", + "integrity": "sha512-zbem3rfRS8BgeNK50Zz5SIQgXzLafiHjOwUAvk/38/o1jHn/V5QAgVUcz884or7WYcPaH3N2CIfUc2u0ul7UcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "enhanced-resolve": "^5.7.0", + "tapable": "^2.2.1", + "tsconfig-paths": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/tsconfig-paths/node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "license": "MIT" + }, + "node_modules/typeorm": { + "version": "0.3.28", + "resolved": "https://registry.npmjs.org/typeorm/-/typeorm-0.3.28.tgz", + "integrity": "sha512-6GH7wXhtfq2D33ZuRXYwIsl/qM5685WZcODZb7noOOcRMteM9KF2x2ap3H0EBjnSV0VO4gNAfJT5Ukp0PkOlvg==", + "license": "MIT", + "dependencies": { + "@sqltools/formatter": "^1.2.5", + "ansis": "^4.2.0", + "app-root-path": "^3.1.0", + "buffer": "^6.0.3", + "dayjs": "^1.11.19", + "debug": "^4.4.3", + "dedent": "^1.7.0", + "dotenv": "^16.6.1", + "glob": "^10.5.0", + "reflect-metadata": "^0.2.2", + "sha.js": "^2.4.12", + "sql-highlight": "^6.1.0", + "tslib": "^2.8.1", + "uuid": "^11.1.0", + "yargs": "^17.7.2" + }, + "bin": { + "typeorm": "cli.js", + "typeorm-ts-node-commonjs": "cli-ts-node-commonjs.js", + "typeorm-ts-node-esm": "cli-ts-node-esm.js" + }, + "engines": { + "node": ">=16.13.0" + }, + "funding": { + "url": "https://opencollective.com/typeorm" + }, + "peerDependencies": { + "@google-cloud/spanner": "^5.18.0 || ^6.0.0 || ^7.0.0 || ^8.0.0", + "@sap/hana-client": "^2.14.22", + "better-sqlite3": "^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 || ^12.0.0", + "ioredis": "^5.0.4", + "mongodb": "^5.8.0 || ^6.0.0", + "mssql": "^9.1.1 || ^10.0.0 || ^11.0.0 || ^12.0.0", + "mysql2": "^2.2.5 || ^3.0.1", + "oracledb": "^6.3.0", + "pg": "^8.5.1", + "pg-native": "^3.0.0", + "pg-query-stream": "^4.0.0", + "redis": "^3.1.1 || ^4.0.0 || ^5.0.14", + "sql.js": "^1.4.0", + "sqlite3": "^5.0.3", + "ts-node": "^10.7.0", + "typeorm-aurora-data-api-driver": "^2.0.0 || ^3.0.0" + }, + "peerDependenciesMeta": { + "@google-cloud/spanner": { + "optional": true + }, + "@sap/hana-client": { + "optional": true + }, + "better-sqlite3": { + "optional": true + }, + "ioredis": { + "optional": true + }, + "mongodb": { + "optional": true + }, + "mssql": { + "optional": true + }, + "mysql2": { + "optional": true + }, + "oracledb": { + "optional": true + }, + "pg": { + "optional": true + }, + "pg-native": { + "optional": true + }, + "pg-query-stream": { + "optional": true + }, + "redis": { + "optional": true + }, + "sql.js": { + "optional": true + }, + "sqlite3": { + "optional": true + }, + "ts-node": { + "optional": true + }, + "typeorm-aurora-data-api-driver": { + "optional": true + } + } + }, + "node_modules/typeorm/node_modules/brace-expansion": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", + "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/typeorm/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/typeorm/node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/typeorm/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/typeorm/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, + "node_modules/typeorm/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/typeorm/node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.57.2.tgz", + "integrity": "sha512-VEPQ0iPgWO/sBaZOU1xo4nuNdODVOajPnTIbog2GKYr31nIlZ0fWPoCQgGfF3ETyBl1vn63F/p50Um9Z4J8O8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.57.2", + "@typescript-eslint/parser": "8.57.2", + "@typescript-eslint/typescript-estree": "8.57.2", + "@typescript-eslint/utils": "8.57.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/uglify-js": { + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/uid": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/uid/-/uid-2.0.2.tgz", + "integrity": "sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g==", + "license": "MIT", + "dependencies": { + "@lukeed/csprng": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/uint8array-extras": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.5.0.tgz", + "integrity": "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT" + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/unrs-resolver": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.11.1.tgz", + "integrity": "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "napi-postinstall": "^0.3.0" + }, + "funding": { + "url": "https://opencollective.com/unrs-resolver" + }, + "optionalDependencies": { + "@unrs/resolver-binding-android-arm-eabi": "1.11.1", + "@unrs/resolver-binding-android-arm64": "1.11.1", + "@unrs/resolver-binding-darwin-arm64": "1.11.1", + "@unrs/resolver-binding-darwin-x64": "1.11.1", + "@unrs/resolver-binding-freebsd-x64": "1.11.1", + "@unrs/resolver-binding-linux-arm-gnueabihf": "1.11.1", + "@unrs/resolver-binding-linux-arm-musleabihf": "1.11.1", + "@unrs/resolver-binding-linux-arm64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-arm64-musl": "1.11.1", + "@unrs/resolver-binding-linux-ppc64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-riscv64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-riscv64-musl": "1.11.1", + "@unrs/resolver-binding-linux-s390x-gnu": "1.11.1", + "@unrs/resolver-binding-linux-x64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-x64-musl": "1.11.1", + "@unrs/resolver-binding-wasm32-wasi": "1.11.1", + "@unrs/resolver-binding-win32-arm64-msvc": "1.11.1", + "@unrs/resolver-binding-win32-ia32-msvc": "1.11.1", + "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.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": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/watchpack": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz", + "integrity": "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "dev": true, + "license": "MIT", + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/webpack": { + "version": "5.105.4", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.105.4.tgz", + "integrity": "sha512-jTywjboN9aHxFlToqb0K0Zs9SbBoW4zRUlGzI2tYNxVYcEi/IPpn+Xi4ye5jTLvX2YeLuic/IvxNot+Q1jMoOw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.8", + "@types/json-schema": "^7.0.15", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.16.0", + "acorn-import-phases": "^1.0.3", + "browserslist": "^4.28.1", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.20.0", + "es-module-lexer": "^2.0.0", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.3.1", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^4.3.3", + "tapable": "^2.3.0", + "terser-webpack-plugin": "^5.3.17", + "watchpack": "^2.5.1", + "webpack-sources": "^3.3.4" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-node-externals": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/webpack-node-externals/-/webpack-node-externals-3.0.0.tgz", + "integrity": "sha512-LnL6Z3GGDPht/AigwRh2dvL9PQPFQ8skEpVrWZXLWBYmqcaojHNN0onvHzie6rq7EWKrrBfPYqNEzTJgiwEQDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/webpack-sources": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.4.tgz", + "integrity": "sha512-7tP1PdV4vF+lYPnkMR0jMY5/la2ub5Fc/8VQrrU+lXkiM6C4TjVfGw7iKfyhnTQOsD+6Q/iKw0eFciziRgD58Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack/node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "dev": true, + "license": "MIT", + "peer": true, + "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/webpack/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==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/webpack/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/webpack/node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "license": "BSD-2-Clause", + "peer": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/webpack/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "license": "BSD-2-Clause", + "peer": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/webpack/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/webpack/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/webpack/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/webpack/node_modules/schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.20", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz", + "integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==", + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "dev": true, + "license": "MIT" + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz", + "integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yoctocolors-cjs": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz", + "integrity": "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/backend/package.json b/backend/package.json new file mode 100644 index 0000000..f70f6bf --- /dev/null +++ b/backend/package.json @@ -0,0 +1,83 @@ +{ + "name": "backend", + "version": "0.0.1", + "description": "", + "author": "", + "private": true, + "license": "UNLICENSED", + "scripts": { + "build": "nest build", + "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", + "start": "nest start", + "start:dev": "nest start --watch", + "start:debug": "nest start --debug --watch", + "start:prod": "node dist/main", + "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", + "test": "jest", + "test:watch": "jest --watch", + "test:cov": "jest --coverage", + "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", + "test:e2e": "jest --config ./test/jest-e2e.json", + "migration:generate": "npx typeorm-ts-node-commonjs migration:generate -d src/data-source.ts", + "migration:run": "npx typeorm-ts-node-commonjs migration:run -d src/data-source.ts", + "migration:revert": "npx typeorm-ts-node-commonjs migration:revert -d src/data-source.ts" + }, + "dependencies": { + "@nestjs/common": "^11.0.1", + "@nestjs/config": "^4.0.3", + "@nestjs/core": "^11.0.1", + "@nestjs/jwt": "^11.0.2", + "@nestjs/platform-express": "^11.0.1", + "@nestjs/throttler": "^6.5.0", + "@nestjs/typeorm": "^11.0.0", + "helmet": "^8.1.0", + "jsonwebtoken": "^9.0.3", + "pg": "^8.20.0", + "reflect-metadata": "^0.2.2", + "rxjs": "^7.8.1", + "typeorm": "^0.3.28" + }, + "devDependencies": { + "@eslint/eslintrc": "^3.2.0", + "@eslint/js": "^9.18.0", + "@nestjs/cli": "^11.0.0", + "@nestjs/schematics": "^11.0.0", + "@nestjs/testing": "^11.0.1", + "@types/express": "^5.0.0", + "@types/jest": "^30.0.0", + "@types/jsonwebtoken": "^9.0.10", + "@types/node": "^22.10.7", + "@types/supertest": "^6.0.2", + "eslint": "^9.18.0", + "eslint-config-prettier": "^10.0.1", + "eslint-plugin-prettier": "^5.2.2", + "globals": "^16.0.0", + "jest": "^30.0.0", + "prettier": "^3.4.2", + "source-map-support": "^0.5.21", + "supertest": "^7.0.0", + "ts-jest": "^29.2.5", + "ts-loader": "^9.5.2", + "ts-node": "^10.9.2", + "tsconfig-paths": "^4.2.0", + "typescript": "^5.7.3", + "typescript-eslint": "^8.20.0" + }, + "jest": { + "moduleFileExtensions": [ + "js", + "json", + "ts" + ], + "rootDir": "src", + "testRegex": ".*\\.spec\\.ts$", + "transform": { + "^.+\\.(t|j)s$": "ts-jest" + }, + "collectCoverageFrom": [ + "**/*.(t|j)s" + ], + "coverageDirectory": "../coverage", + "testEnvironment": "node" + } +} diff --git a/backend/src/admin/admin.controller.ts b/backend/src/admin/admin.controller.ts new file mode 100644 index 0000000..aa37500 --- /dev/null +++ b/backend/src/admin/admin.controller.ts @@ -0,0 +1,301 @@ +import { + Controller, + Get, + Post, + Patch, + Delete, + Param, + Body, + Req, + UseGuards, + BadRequestException, + ParseUUIDPipe, +} from '@nestjs/common'; +import type { Request } from 'express'; +import { SuperAdminGuard } from './super-admin.guard'; +import { ReviewerGuard } from './reviewer.guard'; +import { AdminService } from './admin.service'; +import { AuditLogService } from '../audit-log/audit-log.service'; +import { AuthService } from '../auth/auth.service'; +import { ShopService } from '../shop/shop.service'; + +@Controller('api/admin') +export class AdminController { + constructor( + private readonly adminService: AdminService, + private readonly auditLogService: AuditLogService, + private readonly authService: AuthService, + private readonly shopService: ShopService, + ) {} + + @UseGuards(SuperAdminGuard) + @Get('users') + listUsers() { + return this.adminService.listUsers(); + } + + @UseGuards(SuperAdminGuard) + @Get('users/:id') + getUser(@Param('id', ParseUUIDPipe) id: string) { + return this.adminService.getUser(id); + } + + @UseGuards(SuperAdminGuard) + @Post('users/:id/ban') + async banUser( + @Param('id', ParseUUIDPipe) id: string, + @Req() req: Request, + ) { + const adminId = (req as any).user?.uid; + await this.adminService.banUser(id, adminId); + return { success: true }; + } + + @UseGuards(SuperAdminGuard) + @Patch('users/:id/perms') + async updatePerms( + @Param('id', ParseUUIDPipe) id: string, + @Body() body: { perms?: string }, + @Req() req: Request, + ) { + if (!body.perms || typeof body.perms !== 'string') { + throw new BadRequestException('perms is required'); + } + const adminId = (req as any).user?.uid; + await this.adminService.updatePerms(id, body.perms, adminId); + return { success: true }; + } + + @UseGuards(SuperAdminGuard) + @Post('users/:id/impersonate') + async impersonateUser( + @Param('id', ParseUUIDPipe) id: string, + @Req() req: Request, + ) { + const admin = (req as any).user; + const adminUid = admin?.uid as string; + const adminName = admin?.name as string ?? 'Admin'; + + // Log impersonation on both accounts + await this.auditLogService.log(adminUid, 'admin_impersonate', `Started impersonating user ${id}`); + await this.auditLogService.log(id, 'admin_impersonate', `Admin ${adminName} started impersonating this account`); + + return this.authService.issueImpersonationToken(id, adminUid, adminName); + } + + // ── Projects ── + + @UseGuards(ReviewerGuard) + @Get('projects') + listProjects() { + return this.adminService.listAllProjects(); + } + + @UseGuards(ReviewerGuard) + @Get('projects/:id/hackatime') + getProjectHackatime(@Param('id', ParseUUIDPipe) id: string) { + return this.adminService.getProjectHackatime(id); + } + + @UseGuards(ReviewerGuard) + @Post('projects/:id/review') + async reviewProject( + @Param('id', ParseUUIDPipe) id: string, + @Body() body: { + status?: string; + feedback?: string; + internalNote?: string; + overrideJustification?: string; + overrideHours?: number; + internalHours?: number; + }, + @Req() req: Request, + ) { + const validStatuses = ['approved', 'changes_needed', 'ban']; + if (!body.status || !validStatuses.includes(body.status)) { + throw new BadRequestException(`status must be one of: ${validStatuses.join(', ')}`); + } + const reviewerId = (req as any).user?.uid; + + if (body.status === 'ban') { + return this.adminService.banAndRejectProject( + id, + reviewerId, + body.feedback ?? null, + body.internalNote ?? null, + body.overrideJustification ?? null, + ); + } + + return this.adminService.reviewProject( + id, + reviewerId, + body.status, + body.feedback ?? null, + body.internalNote ?? null, + body.overrideJustification ?? null, + body.overrideHours ?? null, + body.internalHours ?? null, + ); + } + + @UseGuards(ReviewerGuard) + @Get('projects/:id/reviews') + getProjectReviews(@Param('id', ParseUUIDPipe) id: string) { + return this.adminService.getProjectReviews(id, true); + } + + // ── News CRUD ── + + @UseGuards(SuperAdminGuard) + @Get('news') + listNews() { + return this.adminService.listNews(); + } + + @UseGuards(SuperAdminGuard) + @Post('news') + async createNews(@Body() body: { text?: string; displayDate?: string }) { + if (!body.text || !body.displayDate) { + throw new BadRequestException('text and displayDate are required'); + } + return this.adminService.createNews(body.text, body.displayDate); + } + + @UseGuards(SuperAdminGuard) + @Patch('news/:id') + async updateNews( + @Param('id', ParseUUIDPipe) id: string, + @Body() body: { text?: string; displayDate?: string }, + ) { + return this.adminService.updateNews(id, body); + } + + @UseGuards(SuperAdminGuard) + @Delete('news/:id') + async deleteNews(@Param('id', ParseUUIDPipe) id: string) { + await this.adminService.deleteNews(id); + return { success: true }; + } + + // ── Shop CRUD ── + + @UseGuards(SuperAdminGuard) + @Get('shop') + listShopItems() { + return this.adminService.listShopItems(); + } + + @UseGuards(SuperAdminGuard) + @Post('shop') + async createShopItem(@Body() body: { + name?: string; + description?: string; + imageUrl?: string; + priceHours?: number; + stock?: number | null; + estimatedShip?: string | null; + isActive?: boolean; + }) { + if (!body.name || !body.description || !body.imageUrl || body.priceHours == null) { + throw new BadRequestException('name, description, imageUrl, and priceHours are required'); + } + if (!Number.isInteger(body.priceHours) || body.priceHours < 1) { + throw new BadRequestException('priceHours must be a positive integer'); + } + if (body.stock !== undefined && body.stock !== null) { + if (!Number.isInteger(body.stock) || body.stock < 0) { + throw new BadRequestException('stock must be a non-negative integer or null'); + } + } + return this.adminService.createShopItem({ + name: body.name, + description: body.description, + imageUrl: body.imageUrl, + priceHours: body.priceHours, + stock: body.stock, + estimatedShip: body.estimatedShip, + isActive: body.isActive, + }); + } + + @UseGuards(SuperAdminGuard) + @Patch('shop/reorder') + async reorderShopItems(@Body() body: { items?: { id: string; sortOrder: number }[] }) { + if (!Array.isArray(body.items) || body.items.length === 0) { + throw new BadRequestException('items array is required'); + } + for (const item of body.items) { + if (!item.id || typeof item.sortOrder !== 'number' || !Number.isInteger(item.sortOrder) || item.sortOrder < 0) { + throw new BadRequestException('each item must have a valid id and non-negative integer sortOrder'); + } + } + await this.adminService.reorderShopItems(body.items); + return { success: true }; + } + + @UseGuards(SuperAdminGuard) + @Patch('shop/:id') + async updateShopItem( + @Param('id', ParseUUIDPipe) id: string, + @Body() body: { + name?: string; + description?: string; + imageUrl?: string; + priceHours?: number; + stock?: number | null; + estimatedShip?: string | null; + isActive?: boolean; + }, + ) { + if (body.priceHours !== undefined) { + if (!Number.isInteger(body.priceHours) || body.priceHours < 1) { + throw new BadRequestException('priceHours must be a positive integer'); + } + } + if (body.stock !== undefined && body.stock !== null) { + if (!Number.isInteger(body.stock) || body.stock < 0) { + throw new BadRequestException('stock must be a non-negative integer or null'); + } + } + return this.adminService.updateShopItem(id, body); + } + + @UseGuards(SuperAdminGuard) + @Delete('shop/:id') + async deleteShopItem(@Param('id', ParseUUIDPipe) id: string) { + await this.adminService.deleteShopItem(id); + return { success: true }; + } + + // ── Orders / Fulfillment ── + + @UseGuards(SuperAdminGuard) + @Get('orders') + listOrders(@Req() req: Request) { + const query = (req as any).query ?? {}; + return this.shopService.listAllOrders({ + shopItemId: query.shopItemId, + status: query.status, + sortBy: query.sortBy, + }); + } + + @UseGuards(SuperAdminGuard) + @Post('orders/:id/fulfill') + async fulfillOrder(@Param('id', ParseUUIDPipe) id: string) { + return this.shopService.fulfillOrder(id); + } + + @UseGuards(SuperAdminGuard) + @Post('orders/:id/message') + async sendFulfillmentMessage( + @Param('id', ParseUUIDPipe) id: string, + @Body() body: { message?: string }, + ) { + if (!body.message || typeof body.message !== 'string') { + throw new BadRequestException('message is required'); + } + return this.shopService.sendFulfillmentMessage(id, body.message); + } +} diff --git a/backend/src/admin/admin.module.ts b/backend/src/admin/admin.module.ts new file mode 100644 index 0000000..addcd98 --- /dev/null +++ b/backend/src/admin/admin.module.ts @@ -0,0 +1,32 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { AuthModule } from '../auth/auth.module'; +import { RsvpModule } from '../rsvp/rsvp.module'; +import { AuditLogModule } from '../audit-log/audit-log.module'; +import { User } from '../entities/user.entity'; +import { Session } from '../entities/session.entity'; +import { Project } from '../entities/project.entity'; +import { AuditLog } from '../entities/audit-log.entity'; +import { NewsItem } from '../entities/news-item.entity'; +import { ProjectReview } from '../entities/project-review.entity'; +import { ShopItem } from '../entities/shop-item.entity'; +import { Order } from '../entities/order.entity'; +import { Submission } from '../entities/submission.entity'; +import { ShopModule } from '../shop/shop.module'; +import { AdminController } from './admin.controller'; +import { AdminService } from './admin.service'; +import { SuperAdminGuard } from './super-admin.guard'; +import { ReviewerGuard } from './reviewer.guard'; + +@Module({ + imports: [ + TypeOrmModule.forFeature([User, Session, Project, AuditLog, NewsItem, ProjectReview, ShopItem, Order, Submission]), + AuthModule, + RsvpModule, + AuditLogModule, + ShopModule, + ], + controllers: [AdminController], + providers: [AdminService, SuperAdminGuard, ReviewerGuard], +}) +export class AdminModule {} diff --git a/backend/src/admin/admin.service.ts b/backend/src/admin/admin.service.ts new file mode 100644 index 0000000..cc546d8 --- /dev/null +++ b/backend/src/admin/admin.service.ts @@ -0,0 +1,670 @@ +import { + Injectable, + BadRequestException, + Logger, + NotFoundException, +} from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { User } from '../entities/user.entity'; +import { Session } from '../entities/session.entity'; +import { Project } from '../entities/project.entity'; +import { AuditLog } from '../entities/audit-log.entity'; +import { NewsItem } from '../entities/news-item.entity'; +import { ProjectReview } from '../entities/project-review.entity'; +import { ShopItem } from '../entities/shop-item.entity'; +import { Order } from '../entities/order.entity'; +import { Submission } from '../entities/submission.entity'; +import { RsvpService } from '../rsvp/rsvp.service'; +import { AuditLogService } from '../audit-log/audit-log.service'; +import { fetchWithTimeout } from '../fetch.util'; + +const VALID_PERMS = [ + 'User', + 'Helper', + 'Reviewer', + 'Fraud Reviewer', + 'Super Admin', + 'Banned', +] as const; + +@Injectable() +export class AdminService { + private readonly logger = new Logger(AdminService.name); + private readonly hackatimeBaseUrl: string; + private readonly hackatimeAdminKey: string | undefined; + private readonly unifiedApiKey: string | undefined; + private readonly unifiedBaseId = 'app3A5kJwYqxMLOgh'; + + constructor( + private readonly configService: ConfigService, + @InjectRepository(User) private readonly userRepo: Repository, + @InjectRepository(Session) private readonly sessionRepo: Repository, + @InjectRepository(Project) private readonly projectRepo: Repository, + @InjectRepository(AuditLog) private readonly auditLogRepo: Repository, + @InjectRepository(NewsItem) private readonly newsRepo: Repository, + @InjectRepository(ProjectReview) private readonly reviewRepo: Repository, + @InjectRepository(ShopItem) private readonly shopRepo: Repository, + @InjectRepository(Order) private readonly orderRepo: Repository, + @InjectRepository(Submission) private readonly submissionRepo: Repository, + private readonly rsvpService: RsvpService, + private readonly auditLogService: AuditLogService, + ) { + this.hackatimeBaseUrl = this.configService.get( + 'HACKATIME_BASE_URL', + 'https://hackatime.hackclub.com', + ); + this.hackatimeAdminKey = this.configService.get('HACKATIME_ADMIN_API_KEY'); + if (!this.hackatimeAdminKey) { + this.logger.warn('HACKATIME_ADMIN_API_KEY not set — admin Hackatime lookups disabled'); + } + this.unifiedApiKey = this.configService.get('UNIFIED_API_KEY'); + } + + async listUsers(): Promise { + const [users, permsMap] = await Promise.all([ + this.userRepo.find({ order: { createdAt: 'DESC' } }), + this.rsvpService.getAllPerms(), + ]); + return users.map((u) => ({ + id: u.id, + hcaSub: u.hcaSub, + name: u.name, + nickname: u.nickname, + slackId: u.slackId, + email: u.email, + hackatimeConnected: !!u.hackatimeToken, + perms: (u.email ? permsMap.get(u.email.toLowerCase()) : null) ?? null, + createdAt: u.createdAt, + })); + } + + async getUser(userId: string) { + const user = await this.userRepo.findOne({ + where: { id: userId }, + }); + if (!user) throw new NotFoundException('User not found'); + + const projects = await this.projectRepo.find({ + where: { userId }, + order: { createdAt: 'DESC' }, + select: ['id', 'name', 'status', 'projectType', 'createdAt'], + }); + + const orders = await this.orderRepo.find({ + where: { userId }, + order: { createdAt: 'DESC' }, + select: ['id', 'itemName', 'quantity', 'pipesSpent', 'status', 'createdAt'], + }); + + const sessions = await this.sessionRepo.count({ where: { userId } }); + + const auditLogs = await this.auditLogRepo.find({ + where: { userId }, + order: { createdAt: 'DESC' }, + take: 50, + select: ['id', 'action', 'label', 'createdAt'], + }); + + let perms: string | null = null; + try { + if (user.email) { + perms = await this.rsvpService.getPerms(user.email); + } + } catch { + // Airtable lookup failed — don't block the response + } + + return { + id: user.id, + hcaSub: user.hcaSub, + name: user.name, + nickname: user.nickname, + slackId: user.slackId, + email: user.email, + hackatimeConnected: !!user.hackatimeToken, + twoEmails: user.twoEmails, + createdAt: user.createdAt, + updatedAt: user.updatedAt, + pipes: user.pipes ?? 0, + perms, + projects, + orders, + activeSessions: sessions, + auditLogs, + }; + } + + async banUser(userId: string, adminId?: string): Promise { + const user = await this.userRepo.findOne({ where: { id: userId } }); + if (!user) throw new NotFoundException('User not found'); + + // 1. Update Airtable perms to Banned + await this.rsvpService.updatePerms(user.email, 'Banned'); + + // 2. Revoke all sessions for this user + await this.sessionRepo.delete({ userId }); + + // 3. Audit log on the banned user's record + const identifier = user.name || user.slackId || user.hcaSub; + await this.auditLogService.log(userId, 'admin_ban', `Banned user ${identifier}`); + + // 4. Audit log on the admin's record + if (adminId) { + await this.auditLogService.log(adminId, 'admin_ban', `Banned user ${identifier}`); + } + } + + async banAndRejectProject( + projectId: string, + reviewerId: string, + feedback: string | null, + internalNote: string | null, + overrideJustification: string | null, + ) { + const project = await this.projectRepo.findOne({ + where: { id: projectId }, + relations: ['user'], + }); + if (!project) throw new NotFoundException('Project not found'); + + // 1. Reject the project + project.status = 'changes_needed'; + await this.projectRepo.save(project); + + // 2. Save review record + const review = this.reviewRepo.create({ + projectId, + reviewerId, + status: 'ban', + feedback: feedback || null, + internalNote: internalNote || null, + overrideJustification: overrideJustification || null, + }); + await this.reviewRepo.save(review); + + // 3. Ban the user + await this.banUser(project.userId); + + // 4. Audit logs + await this.auditLogService.log(project.userId, 'project_reviewed', `Project "${project.name}" was rejected`); + await this.auditLogService.log(project.userId, 'admin_ban', `Banned via project review of "${project.name}"`); + + return { success: true }; + } + + async updatePerms(userId: string, perms: string, adminId?: string): Promise { + if (!VALID_PERMS.includes(perms as any)) { + throw new BadRequestException( + `Invalid perms value. Must be one of: ${VALID_PERMS.join(', ')}`, + ); + } + + const user = await this.userRepo.findOne({ where: { id: userId } }); + if (!user) throw new NotFoundException('User not found'); + + await this.rsvpService.updatePerms(user.email, perms); + + const identifier = user.name || user.slackId || user.hcaSub; + await this.auditLogService.log(userId, 'admin_perms_change', `Changed ${identifier} perms to ${perms}`); + + if (adminId) { + await this.auditLogService.log(adminId, 'admin_perms_change', `Changed ${identifier} perms to ${perms}`); + } + } + + // ── Projects ── + + async listAllProjects() { + const projects = await this.projectRepo.find({ + order: { createdAt: 'DESC' }, + relations: ['user'], + }); + + const statusCounts = { + unshipped: 0, + unreviewed: 0, + changes_needed: 0, + approved: 0, + }; + + // Fetch latest submission for each project in a single query + const latestSubmissions = await this.submissionRepo + .createQueryBuilder('s') + .distinctOn(['s.project_id']) + .orderBy('s.project_id') + .addOrderBy('s.created_at', 'DESC') + .getMany(); + const submissionMap = new Map(latestSubmissions.map((s) => [s.projectId, s])); + + const mapped = projects.map((p) => { + if (p.status in statusCounts) { + statusCounts[p.status as keyof typeof statusCounts]++; + } + const latestSub = submissionMap.get(p.id); + return { + id: p.id, + name: p.name, + description: p.description, + projectType: p.projectType, + status: p.status, + codeUrl: p.codeUrl, + demoUrl: p.demoUrl, + readmeUrl: p.readmeUrl, + screenshot1Url: p.screenshot1Url, + screenshot2Url: p.screenshot2Url, + hackatimeProjectName: p.hackatimeProjectName, + isUpdate: p.isUpdate, + otherHcProgram: p.otherHcProgram, + aiUse: p.aiUse, + createdAt: p.createdAt, + updatedAt: p.updatedAt, + user: { + id: p.user?.id, + name: p.user?.name, + slackId: p.user?.slackId, + }, + latestSubmission: latestSub ? { + id: latestSub.id, + changeDescription: latestSub.changeDescription, + minHoursConfirmed: latestSub.minHoursConfirmed, + status: latestSub.status, + createdAt: latestSub.createdAt, + } : null, + }; + }); + + return { statusCounts, projects: mapped }; + } + + async reviewProject( + projectId: string, + reviewerId: string, + status: string, + feedback: string | null, + internalNote: string | null, + overrideJustification: string | null, + overrideHours: number | null, + internalHours: number | null, + ) { + const project = await this.projectRepo.findOne({ + where: { id: projectId }, + relations: ['user'], + }); + if (!project) throw new NotFoundException('Project not found'); + + // Find the latest unreviewed submission for this project + const submission = await this.submissionRepo.findOne({ + where: { projectId, status: 'unreviewed' }, + order: { createdAt: 'DESC' }, + }); + + // 1. Update project status and hours + project.status = status; + if (overrideHours !== null && overrideHours !== undefined) { + project.overrideHours = Math.round(overrideHours * 10) / 10; + } + if (internalHours !== null && internalHours !== undefined) { + project.internalHours = Math.round(internalHours * 10) / 10; + } + await this.projectRepo.save(project); + + // 2. Grant pipes as delta on this submission + // Pipes granted = overrideHours for THIS submission minus what was already granted on previous submissions. + // The project's pipesGranted tracks the cumulative total. + if (status === 'approved' && project.overrideHours != null && project.overrideHours > 0) { + const totalPipesTarget = Math.floor(project.overrideHours); + const previouslyGranted = project.pipesGranted ?? 0; + const delta = totalPipesTarget - previouslyGranted; + if (delta > 0) { + await this.userRepo.increment({ id: project.userId }, 'pipes', delta); + project.pipesGranted = totalPipesTarget; + await this.projectRepo.save(project); + + // Track what this submission granted + if (submission) { + submission.pipesGranted = delta; + } + } + } + + // 3. Update the submission status and hours + if (submission) { + submission.status = status; + if (overrideHours !== null && overrideHours !== undefined) { + submission.overrideHours = Math.round(overrideHours * 10) / 10; + } + if (internalHours !== null && internalHours !== undefined) { + submission.internalHours = Math.round(internalHours * 10) / 10; + } + await this.submissionRepo.save(submission); + } + + // 4. Save the review record (linked to submission if one exists) + const review = this.reviewRepo.create({ + projectId, + reviewerId, + submissionId: submission?.id ?? null, + status, + feedback: feedback || null, + internalNote: internalNote || null, + overrideJustification: overrideJustification || null, + }); + await this.reviewRepo.save(review); + + // 5. Audit log to the project owner (not the reviewer) + const label = + status === 'approved' + ? `Project "${project.name}" was approved` + : `Project "${project.name}" received feedback`; + await this.auditLogService.log(project.userId, 'project_reviewed', label); + + // 6. Sync approval date to Airtable for Loops + if (status === 'approved' && project.user?.email) { + this.rsvpService.updateDateField(project.user.email, 'Loops - beestApprovedProject'); + } + + return { success: true }; + } + + async getProjectReviews(projectId: string, includeInternal: boolean) { + const reviews = await this.reviewRepo.find({ + where: { projectId }, + order: { createdAt: 'DESC' }, + relations: ['reviewer'], + }); + + return reviews.map((r) => ({ + id: r.id, + status: r.status, + feedback: r.feedback, + ...(includeInternal ? { internalNote: r.internalNote } : {}), + overrideJustification: r.overrideJustification, + reviewerName: r.reviewer?.name ?? null, + createdAt: r.createdAt, + })); + } + + // ── Unified Airtable duplicate check ── + + /** + * Checks the Unified Airtable "Approved Projects" table for a matching Code URL. + * + * Security constraints: + * - This method is private — only callable within AdminService + * - Only called from getProjectHackatime, which is behind SuperAdminGuard + * - Only accepts HTTPS URLs (rejects anything else) + * - The codeUrl is taken from the project's DB record, never from user input + * - Returns only boolean match/error — no Airtable record data is ever exposed + */ + private async checkUnifiedDuplicate( + codeUrl: string, + ): Promise<{ duplicate: boolean; error: boolean }> { + if (!this.unifiedApiKey || !codeUrl) { + return { duplicate: false, error: true }; + } + + // Only allow https:// URLs — reject anything that could be a formula injection + try { + const parsed = new URL(codeUrl); + if (parsed.protocol !== 'https:') { + return { duplicate: false, error: true }; + } + } catch { + return { duplicate: false, error: true }; + } + + // Escape for Airtable formula: double any backslashes, then escape single quotes + const escaped = codeUrl.replace(/\\/g, '\\\\').replace(/'/g, "\\'"); + const formula = `{Code URL} = '${escaped}'`; + + try { + const params = new URLSearchParams({ + filterByFormula: formula, + maxRecords: '1', + 'fields[]': 'Code URL', + }); + const url = `https://api.airtable.com/v0/${this.unifiedBaseId}/Approved%20Projects?${params}`; + this.logger.log(`Unified check: formula=${formula}`); + const res = await fetchWithTimeout(url, { + headers: { Authorization: `Bearer ${this.unifiedApiKey}` }, + }); + if (!res.ok) { + const body = await res.text(); + this.logger.warn(`Unified check failed (${res.status}): ${body}`); + return { duplicate: false, error: true }; + } + const data = await res.json(); + const records: any[] = data?.records ?? []; + this.logger.log(`Unified check: ${records.length} records found`); + // Only expose match/no-match — never leak record contents + return { duplicate: records.length > 0, error: false }; + } catch { + return { duplicate: false, error: true }; + } + } + + // ── Hackatime admin lookup ── + + private async hackatimeGet(path: string): Promise { + return fetchWithTimeout(`${this.hackatimeBaseUrl}${path}`, { + headers: { Authorization: `Bearer ${this.hackatimeAdminKey}` }, + }); + } + + private async hackatimePost(path: string, body: object): Promise { + return fetchWithTimeout(`${this.hackatimeBaseUrl}${path}`, { + method: 'POST', + headers: { + Authorization: `Bearer ${this.hackatimeAdminKey}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify(body), + }); + } + + async getProjectHackatime(projectId: string) { + if (!this.hackatimeAdminKey) { + throw new BadRequestException('Hackatime admin API key not configured'); + } + + const project = await this.projectRepo.findOne({ + where: { id: projectId }, + relations: ['user'], + }); + if (!project) throw new NotFoundException('Project not found'); + + const hackatimeNames: string[] = project.hackatimeProjectName ?? []; + const user = project.user; + if (!user) { + return { projectId, hackatimeProjects: [], totalHours: 0, previousApprovedHours: 0, trustLevel: null, unifiedDuplicate: false, unifiedError: true }; + } + + try { + // 1. Resolve Hackatime user ID — prefer stored ID, fall back to email lookup, then OAuth token + let hackatimeUserId: string | number | null = user.hackatimeUserId ?? null; + if (!hackatimeUserId && user.email) { + try { + const emailRes = await this.hackatimePost( + '/api/admin/v1/user/get_user_by_email', + { email: user.email }, + ); + if (emailRes.ok) { + const emailData = await emailRes.json(); + hackatimeUserId = emailData.user_id ?? emailData?.data?.user_id ?? null; + } + } catch (err) { + this.logger.warn(`Hackatime email lookup failed for project ${projectId}: ${err}`); + } + } + // Last resort: use the user's own Hackatime OAuth token to resolve their ID + if (!hackatimeUserId && user.hackatimeToken) { + try { + const meRes = await fetchWithTimeout( + `${this.hackatimeBaseUrl}/api/v1/authenticated/me`, + { headers: { Authorization: `Bearer ${user.hackatimeToken}` } }, + ); + if (meRes.ok) { + const meData = await meRes.json(); + const d = meData?.data ?? meData; + hackatimeUserId = d?.id?.toString() ?? d?.user_id?.toString() ?? null; + // Persist for future lookups + if (hackatimeUserId) { + user.hackatimeUserId = String(hackatimeUserId); + await this.userRepo.save(user); + } + } + } catch (err) { + this.logger.warn(`Hackatime OAuth /me fallback failed for project ${projectId}: ${err}`); + } + } + if (!hackatimeUserId) { + return { projectId, hackatimeProjects: [], totalHours: 0, previousApprovedHours: 0, trustLevel: null, unifiedDuplicate: false, unifiedError: true }; + } + + // 2. Get user info (trust level), projects, and Unified duplicate check in parallel + const [infoRes, projectsRes, unifiedResult] = await Promise.all([ + this.hackatimeGet(`/api/admin/v1/user/info?user_id=${hackatimeUserId}`), + this.hackatimeGet(`/api/admin/v1/user/projects?user_id=${hackatimeUserId}`), + this.checkUnifiedDuplicate(project.codeUrl ?? ''), + ]); + + let trustLevel: string | null = null; + if (infoRes.ok) { + const infoData = await infoRes.json(); + trustLevel = infoData?.user?.trust_level ?? infoData?.data?.trust_level ?? infoData?.trust_level ?? null; + } + + let matched: { name: string; hours: number; languages: string[] }[] = []; + if (projectsRes.ok) { + const projData = await projectsRes.json(); + const allProjects: { + name: string; + total_duration: number; + languages: string[]; + }[] = projData?.projects ?? projData?.data ?? []; + + if (hackatimeNames.length > 0) { + const nameSet = new Set(hackatimeNames); + matched = allProjects + .filter((p) => nameSet.has(p.name)) + .map((p) => ({ + name: p.name, + hours: Math.round(((p.total_duration ?? (p as any).total_seconds ?? 0) / 3600) * 10) / 10, + languages: p.languages ?? [], + })); + } + } + + const totalHours = Math.round( + matched.reduce((sum, p) => sum + p.hours, 0) * 10, + ) / 10; + + // Calculate previous approved hours for delta display on resubmissions + const lastApprovedSub = await this.submissionRepo.findOne({ + where: { projectId, status: 'approved' }, + order: { createdAt: 'DESC' }, + select: ['id', 'overrideHours'], + }); + const previousApprovedHours = lastApprovedSub?.overrideHours ?? 0; + + return { projectId, hackatimeProjects: matched, totalHours, previousApprovedHours, trustLevel, unifiedDuplicate: unifiedResult.duplicate, unifiedError: unifiedResult.error }; + } catch (err) { + this.logger.error(`Hackatime admin lookup error for project ${projectId}: ${err}`); + return { projectId, hackatimeProjects: [], totalHours: 0, previousApprovedHours: 0, trustLevel: null, unifiedDuplicate: false, unifiedError: true }; + } + } + + // ── News CRUD ── + + async listNews(): Promise { + return this.newsRepo.find({ order: { displayDate: 'DESC', createdAt: 'DESC' } }); + } + + async createNews(text: string, displayDate: string): Promise { + const item = this.newsRepo.create({ text, displayDate }); + return this.newsRepo.save(item); + } + + async updateNews(id: string, data: { text?: string; displayDate?: string }): Promise { + const item = await this.newsRepo.findOne({ where: { id } }); + if (!item) throw new NotFoundException('News item not found'); + if (data.text !== undefined) item.text = data.text; + if (data.displayDate !== undefined) item.displayDate = data.displayDate; + return this.newsRepo.save(item); + } + + async deleteNews(id: string): Promise { + const item = await this.newsRepo.findOne({ where: { id } }); + if (!item) throw new NotFoundException('News item not found'); + await this.newsRepo.remove(item); + } + + // ── Shop CRUD ── + + async listShopItems(): Promise { + return this.shopRepo.find({ order: { sortOrder: 'ASC' } }); + } + + async createShopItem(data: { + name: string; + description: string; + imageUrl: string; + priceHours: number; + stock?: number | null; + estimatedShip?: string | null; + isActive?: boolean; + }): Promise { + const maxOrder = await this.shopRepo + .createQueryBuilder('s') + .select('MAX(s.sortOrder)', 'max') + .getRawOne(); + const sortOrder = (maxOrder?.max ?? -1) + 1; + + const item = this.shopRepo.create({ + name: data.name, + description: data.description, + imageUrl: data.imageUrl, + priceHours: data.priceHours, + stock: data.stock ?? null, + estimatedShip: data.estimatedShip ?? null, + isActive: data.isActive ?? true, + sortOrder, + }); + return this.shopRepo.save(item); + } + + async updateShopItem(id: string, data: { + name?: string; + description?: string; + imageUrl?: string; + priceHours?: number; + stock?: number | null; + estimatedShip?: string | null; + isActive?: boolean; + }): Promise { + const item = await this.shopRepo.findOne({ where: { id } }); + if (!item) throw new NotFoundException('Shop item not found'); + if (data.name !== undefined) item.name = data.name; + if (data.description !== undefined) item.description = data.description; + if (data.imageUrl !== undefined) item.imageUrl = data.imageUrl; + if (data.priceHours !== undefined) item.priceHours = data.priceHours; + if (data.stock !== undefined) item.stock = data.stock; + if (data.estimatedShip !== undefined) item.estimatedShip = data.estimatedShip; + if (data.isActive !== undefined) item.isActive = data.isActive; + return this.shopRepo.save(item); + } + + async deleteShopItem(id: string): Promise { + const item = await this.shopRepo.findOne({ where: { id } }); + if (!item) throw new NotFoundException('Shop item not found'); + await this.shopRepo.remove(item); + } + + async reorderShopItems(items: { id: string; sortOrder: number }[]): Promise { + await Promise.all( + items.map((i) => this.shopRepo.update(i.id, { sortOrder: i.sortOrder })), + ); + } +} diff --git a/backend/src/admin/reviewer.guard.ts b/backend/src/admin/reviewer.guard.ts new file mode 100644 index 0000000..6382b1f --- /dev/null +++ b/backend/src/admin/reviewer.guard.ts @@ -0,0 +1,51 @@ +import { + CanActivate, + ExecutionContext, + Injectable, + ForbiddenException, + UnauthorizedException, +} from '@nestjs/common'; +import { AuthService } from '../auth/auth.service'; +import { RsvpService } from '../rsvp/rsvp.service'; + +const REVIEWER_ROLES = ['Super Admin', 'Reviewer', 'Fraud Reviewer']; + +/** + * Guard that requires a valid JWT AND Reviewer-level+ Perms in Airtable. + * Allows Super Admin, Reviewer, and Fraud Reviewer. + */ +@Injectable() +export class ReviewerGuard implements CanActivate { + constructor( + private readonly authService: AuthService, + private readonly rsvpService: RsvpService, + ) {} + + async canActivate(context: ExecutionContext): Promise { + const request = context.switchToHttp().getRequest(); + const authHeader = request.headers.authorization; + + if (!authHeader?.startsWith('Bearer ')) { + throw new UnauthorizedException(); + } + + let user: Record; + try { + const token = authHeader.split(' ')[1]; + user = this.authService.verifyToken(token); + } catch { + throw new UnauthorizedException(); + } + + const email = user?.email as string; + if (!email) throw new ForbiddenException(); + + const perms = await this.rsvpService.getPerms(email); + if (!perms || !REVIEWER_ROLES.includes(perms)) { + throw new ForbiddenException(); + } + + request.user = user; + return true; + } +} diff --git a/backend/src/admin/super-admin.guard.ts b/backend/src/admin/super-admin.guard.ts new file mode 100644 index 0000000..89998f6 --- /dev/null +++ b/backend/src/admin/super-admin.guard.ts @@ -0,0 +1,49 @@ +import { + CanActivate, + ExecutionContext, + Injectable, + ForbiddenException, + UnauthorizedException, +} from '@nestjs/common'; +import { AuthService } from '../auth/auth.service'; +import { RsvpService } from '../rsvp/rsvp.service'; + +/** + * Guard that requires a valid JWT AND Super Admin Perms in Airtable. + * Checks Airtable on every request — no caching, so revocations are instant. + */ +@Injectable() +export class SuperAdminGuard implements CanActivate { + constructor( + private readonly authService: AuthService, + private readonly rsvpService: RsvpService, + ) {} + + async canActivate(context: ExecutionContext): Promise { + const request = context.switchToHttp().getRequest(); + const authHeader = request.headers.authorization; + + if (!authHeader?.startsWith('Bearer ')) { + throw new UnauthorizedException(); + } + + let user: Record; + try { + const token = authHeader.split(' ')[1]; + user = this.authService.verifyToken(token); + } catch { + throw new UnauthorizedException(); + } + + const email = user?.email as string; + if (!email) throw new ForbiddenException(); + + const perms = await this.rsvpService.getPerms(email); + if (perms !== 'Super Admin') { + throw new ForbiddenException(); + } + + request.user = user; + return true; + } +} diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts new file mode 100644 index 0000000..a098a30 --- /dev/null +++ b/backend/src/app.module.ts @@ -0,0 +1,53 @@ +import { Module } from '@nestjs/common'; +import { ConfigModule, ConfigService } from '@nestjs/config'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { RsvpModule } from './rsvp/rsvp.module'; +import { AuthModule } from './auth/auth.module'; +import { HackatimeModule } from './hackatime/hackatime.module'; +import { OnboardingModule } from './onboarding/onboarding.module'; +import { ProjectsModule } from './projects/projects.module'; +import { AuditLogModule } from './audit-log/audit-log.module'; +import { AdminModule } from './admin/admin.module'; +import { NewsModule } from './news/news.module'; +import { ShopModule } from './shop/shop.module'; +import { User } from './entities/user.entity'; +import { Session } from './entities/session.entity'; +import { Project } from './entities/project.entity'; +import { AuditLog } from './entities/audit-log.entity'; +import { NewsItem } from './entities/news-item.entity'; +import { ProjectReview } from './entities/project-review.entity'; +import { Comment } from './entities/comment.entity'; +import { ShopItem } from './entities/shop-item.entity'; +import { Order } from './entities/order.entity'; +import { FulfillmentUpdate } from './entities/fulfillment-update.entity'; +import { Submission } from './entities/submission.entity'; +import { HealthController } from './health.controller'; + +@Module({ + controllers: [HealthController], + imports: [ + ConfigModule.forRoot({ isGlobal: true }), + TypeOrmModule.forRootAsync({ + imports: [ConfigModule], + inject: [ConfigService], + useFactory: (config: ConfigService) => ({ + type: 'postgres', + url: config.getOrThrow('DATABASE_URL'), + entities: [User, Session, Project, AuditLog, NewsItem, ProjectReview, Comment, ShopItem, Order, FulfillmentUpdate, Submission], + migrations: [__dirname + '/migrations/*{.ts,.js}'], + migrationsRun: true, + synchronize: false, + }), + }), + RsvpModule, + AuthModule, + HackatimeModule, + OnboardingModule, + ProjectsModule, + AuditLogModule, + AdminModule, + NewsModule, + ShopModule, + ], +}) +export class AppModule {} diff --git a/backend/src/audit-log/audit-log.controller.ts b/backend/src/audit-log/audit-log.controller.ts new file mode 100644 index 0000000..e813623 --- /dev/null +++ b/backend/src/audit-log/audit-log.controller.ts @@ -0,0 +1,26 @@ +import { + Controller, + Get, + Req, + UseGuards, + UnauthorizedException, +} from '@nestjs/common'; +import { Throttle } from '@nestjs/throttler'; +import type { Request } from 'express'; +import { JwtAuthGuard } from '../auth/jwt-auth.guard'; +import { AuditLogService } from './audit-log.service'; + +@Controller('api/audit-log') +export class AuditLogController { + constructor(private readonly auditLogService: AuditLogService) {} + + @Throttle({ default: { limit: 30, ttl: 60000 } }) + @UseGuards(JwtAuthGuard) + @Get() + async list(@Req() req: Request) { + const userId = (req as any).user?.uid; + if (!userId) throw new UnauthorizedException('No user identity'); + + return this.auditLogService.getForUser(userId); + } +} diff --git a/backend/src/audit-log/audit-log.module.ts b/backend/src/audit-log/audit-log.module.ts new file mode 100644 index 0000000..84e6da4 --- /dev/null +++ b/backend/src/audit-log/audit-log.module.ts @@ -0,0 +1,14 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { AuthModule } from '../auth/auth.module'; +import { AuditLog } from '../entities/audit-log.entity'; +import { AuditLogController } from './audit-log.controller'; +import { AuditLogService } from './audit-log.service'; + +@Module({ + imports: [AuthModule, TypeOrmModule.forFeature([AuditLog])], + controllers: [AuditLogController], + providers: [AuditLogService], + exports: [AuditLogService], +}) +export class AuditLogModule {} diff --git a/backend/src/audit-log/audit-log.service.ts b/backend/src/audit-log/audit-log.service.ts new file mode 100644 index 0000000..7f917a9 --- /dev/null +++ b/backend/src/audit-log/audit-log.service.ts @@ -0,0 +1,31 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { AuditLog, AuditAction } from '../entities/audit-log.entity'; + +@Injectable() +export class AuditLogService { + constructor( + @InjectRepository(AuditLog) + private auditLogRepo: Repository, + ) {} + + async log(userId: string, action: AuditAction, label: string, impersonatorName?: string): Promise { + const prefix = impersonatorName ? `[${impersonatorName} performed an action on your behalf] ` : ''; + const entry = this.auditLogRepo.create({ + userId, + action, + label: (prefix + label).replace(/[<>"'`&\\]/g, '').replace(/\0/g, '').trim().slice(0, 255), + }); + await this.auditLogRepo.save(entry); + } + + async getForUser(userId: string, limit = 50) { + return this.auditLogRepo.find({ + where: { userId }, + order: { createdAt: 'DESC' }, + take: limit, + select: ['id', 'action', 'label', 'createdAt'], + }); + } +} diff --git a/backend/src/auth/auth.controller.ts b/backend/src/auth/auth.controller.ts new file mode 100644 index 0000000..5115fdb --- /dev/null +++ b/backend/src/auth/auth.controller.ts @@ -0,0 +1,171 @@ +import { + Controller, + Get, + Post, + Patch, + Body, + Req, + Query, + UseGuards, + UnauthorizedException, + BadRequestException, + ForbiddenException, +} from '@nestjs/common'; +import { Throttle } from '@nestjs/throttler'; +import type { Request } from 'express'; +import { AuthService } from './auth.service'; +import { JwtAuthGuard } from './jwt-auth.guard'; +import { RsvpService } from '../rsvp/rsvp.service'; + +@Controller('api/auth') +export class AuthController { + constructor( + private readonly authService: AuthService, + private readonly rsvpService: RsvpService, + ) {} + + @Throttle({ default: { limit: 10, ttl: 60000 } }) + @Post('start') + start(@Body() body: { email?: string }) { + return this.authService.startAuth(body.email); + } + + @Throttle({ default: { limit: 10, ttl: 60000 } }) + @Post('handle-callback') + async handleCallback( + @Body() + body: { + code: string; + state: string; + storedState: string; + }, + ) { + if (!body.code) { + throw new BadRequestException('Authorization code is required'); + } + if (!body.state || !body.storedState) { + throw new BadRequestException('State parameters are required'); + } + try { + return await this.authService.handleCallback( + body.code, + body.state, + body.storedState, + ); + } catch { + throw new UnauthorizedException('Authentication failed'); + } + } + + /** + * Exchange a refresh token for a new JWT + rotated refresh token. + */ + @Throttle({ default: { limit: 10, ttl: 60000 } }) + @Post('refresh') + async refresh(@Body() body: { refreshToken: string }) { + if (!body.refreshToken) { + throw new BadRequestException('Refresh token is required'); + } + try { + return await this.authService.refreshAuth(body.refreshToken); + } catch { + throw new UnauthorizedException('Invalid refresh token'); + } + } + + @UseGuards(JwtAuthGuard) + @Get('me') + async me(@Req() req: Request) { + const user = (req as any).user; + // Check if user has been banned since the JWT was issued + try { + const perms = await this.rsvpService.getPerms(user.email); + if (perms === 'Banned') { + throw new UnauthorizedException('Account banned'); + } + } catch (err) { + if (err instanceof UnauthorizedException) throw err; + // Airtable lookup failed — don't block the response + } + // Include impersonation context if present so the frontend can show it + const result: Record = { ...user }; + if (user.impersonator_uid) { + result.impersonator_uid = user.impersonator_uid; + result.impersonator_name = user.impersonator_name; + } + return result; + } + + @UseGuards(JwtAuthGuard) + @Get('shipping-eligibility') + async shippingEligibility(@Req() req: Request) { + const user = (req as any).user; + return { + hasAddress: !!user.has_address, + hasBirthdate: !!user.has_birthdate, + eligible: !!user.has_address && !!user.has_birthdate, + addressPortalUrl: 'https://auth.hackclub.com/portal/address', + }; + } + + @UseGuards(JwtAuthGuard) + @Post('rsvp') + async rsvpFromSession(@Req() req: Request) { + const email = (req as any).user?.email; + if (!email) { + throw new BadRequestException('No email in token'); + } + return this.rsvpService.createRsvp(email); + } + + @UseGuards(JwtAuthGuard) + @Get('scope') + async checkScope( + @Req() req: Request, + @Query('scope') scope: string, + ) { + const email = (req as any).user?.email; + if (!email) throw new ForbiddenException(); + + const perms = await this.rsvpService.getPerms(email); + + const scopeRequirements: Record = { + admin: ['Super Admin'], + reviewer: ['Super Admin', 'Reviewer', 'Fraud Reviewer'], + }; + + const allowed = scopeRequirements[scope]; + if (!allowed || !perms || !allowed.includes(perms)) { + throw new ForbiddenException(); + } + + return { allowed: true, perms }; + } + + @Throttle({ default: { limit: 10, ttl: 60000 } }) + @UseGuards(JwtAuthGuard) + @Patch('nickname') + async updateNickname( + @Req() req: Request, + @Body() body: { nickname?: string }, + ) { + const uid = (req as any).user?.uid; + if (!uid) throw new UnauthorizedException(); + const nickname = (body.nickname ?? '').trim(); + if (!nickname || nickname.length > 50) { + throw new BadRequestException('Nickname must be 1–50 characters'); + } + return this.authService.updateNickname(uid, nickname); + } + + /** + * Invalidates the session's refresh token. The proxy clears cookies. + */ + @Post('logout') + async logout(@Body() body: { refreshToken?: string }) { + if (body.refreshToken) { + await this.authService.invalidateSession(body.refreshToken); + } + return { success: true }; + } +} diff --git a/backend/src/auth/auth.module.ts b/backend/src/auth/auth.module.ts new file mode 100644 index 0000000..b0d52f4 --- /dev/null +++ b/backend/src/auth/auth.module.ts @@ -0,0 +1,46 @@ +import { Module } from '@nestjs/common'; +import { JwtModule } from '@nestjs/jwt'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { ThrottlerModule, ThrottlerGuard } from '@nestjs/throttler'; +import { APP_GUARD } from '@nestjs/core'; +import { ConfigModule, ConfigService } from '@nestjs/config'; +import { RsvpModule } from '../rsvp/rsvp.module'; +import { User } from '../entities/user.entity'; +import { Session } from '../entities/session.entity'; +import { AuthService } from './auth.service'; +import { AuthController } from './auth.controller'; +import { JwtAuthGuard } from './jwt-auth.guard'; + +@Module({ + imports: [ + TypeOrmModule.forFeature([User, Session]), + JwtModule.registerAsync({ + imports: [ConfigModule], + inject: [ConfigService], + useFactory: (configService: ConfigService) => ({ + secret: configService.getOrThrow('JWT_SECRET'), + signOptions: { + expiresIn: '1h', + issuer: 'beest', + audience: 'beest', + }, + verifyOptions: { + issuer: 'beest', + audience: 'beest', + }, + }), + }), + ThrottlerModule.forRoot({ + throttlers: [{ ttl: 60000, limit: 30 }], + }), + RsvpModule, + ], + controllers: [AuthController], + providers: [ + AuthService, + JwtAuthGuard, + { provide: APP_GUARD, useClass: ThrottlerGuard }, + ], + exports: [AuthService, JwtAuthGuard], +}) +export class AuthModule {} diff --git a/backend/src/auth/auth.service.ts b/backend/src/auth/auth.service.ts new file mode 100644 index 0000000..e8c78ce --- /dev/null +++ b/backend/src/auth/auth.service.ts @@ -0,0 +1,386 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { JwtService } from '@nestjs/jwt'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { createHash, createHmac, randomBytes, timingSafeEqual } from 'crypto'; +import { fetchWithTimeout } from '../fetch.util'; +import { RsvpService } from '../rsvp/rsvp.service'; +import { User } from '../entities/user.entity'; +import { Session } from '../entities/session.entity'; + +const ALLOWED_REDIRECTS = new Set(['/home', '/tutorial']); + +const EMAIL_RE = + /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/; + +const REFRESH_TOKEN_EXPIRY_MS = 90 * 24 * 60 * 60 * 1000; // 90 days + +@Injectable() +export class AuthService { + private readonly logger = new Logger(AuthService.name); + private readonly clientId: string; + private readonly clientSecret: string; + private readonly redirectUri: string; + private readonly jwtSecret: string; + + private readonly authorizeUrl = + 'https://auth.hackclub.com/oauth/authorize'; + private readonly tokenUrl = 'https://auth.hackclub.com/oauth/token'; + private readonly userinfoUrl = 'https://auth.hackclub.com/oauth/userinfo'; + + private readonly scopes = [ + 'openid', + 'email', + 'name', + 'profile', + 'birthdate', + 'address', + 'verification_status', + 'slack_id', + 'basic_info', + ].join(' '); + + constructor( + private configService: ConfigService, + private jwtService: JwtService, + private rsvpService: RsvpService, + @InjectRepository(User) + private userRepo: Repository, + @InjectRepository(Session) + private sessionRepo: Repository, + ) { + this.clientId = this.configService.getOrThrow('CLIENT_ID'); + this.clientSecret = this.configService.getOrThrow('CLIENT_SECRET'); + this.redirectUri = this.configService.get( + 'REDIRECT_URI', + 'http://localhost:5173/oauth/callback', + ); + this.jwtSecret = this.configService.getOrThrow('JWT_SECRET'); + } + + private signState(state: string): string { + return createHmac('sha256', this.jwtSecret) + .update(`hca:${state}`) + .digest('hex'); + } + + startAuth(email?: string): { url: string; state: string } { + const state = crypto.randomUUID(); + const signature = this.signState(state); + const signedState = `${state}.${signature}`; + + const sanitizedEmail = + email && EMAIL_RE.test(email.trim()) ? email.trim() : undefined; + + const params = new URLSearchParams({ + client_id: this.clientId, + redirect_uri: this.redirectUri, + response_type: 'code', + scope: this.scopes, + state: signedState, + return_to: '/join/beest', + }); + + if (sanitizedEmail) { + params.set('login_hint', sanitizedEmail); + } + + return { + url: `${this.authorizeUrl}?${params.toString()}`, + state, + }; + } + + async handleCallback( + code: string, + returnedSignedState: string, + cookieState: string, + ): Promise<{ token: string; refreshToken: string; redirectTo: string }> { + // 1. Verify state + const dotIndex = returnedSignedState.lastIndexOf('.'); + if (dotIndex === -1) { + throw new Error('Malformed state parameter'); + } + + const stateValue = returnedSignedState.substring(0, dotIndex); + const signature = returnedSignedState.substring(dotIndex + 1); + + const stateBuffer = Buffer.from(stateValue); + const cookieBuffer = Buffer.from(cookieState); + if ( + stateBuffer.length !== cookieBuffer.length || + !timingSafeEqual(stateBuffer, cookieBuffer) + ) { + throw new Error('State mismatch'); + } + + const expectedSignature = this.signState(stateValue); + const sigBuffer = Buffer.from(signature); + const expectedBuffer = Buffer.from(expectedSignature); + if ( + sigBuffer.length !== expectedBuffer.length || + !timingSafeEqual(sigBuffer, expectedBuffer) + ) { + throw new Error('Invalid state signature'); + } + + // 2. Exchange code for tokens + const tokenResponse = await fetchWithTimeout(this.tokenUrl, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: new URLSearchParams({ + grant_type: 'authorization_code', + code, + redirect_uri: this.redirectUri, + client_id: this.clientId, + client_secret: this.clientSecret, + }), + }); + + if (!tokenResponse.ok) { + this.logger.error(`Token exchange failed: ${tokenResponse.status}`); + throw new Error('Token exchange failed'); + } + + const tokens = await tokenResponse.json().catch(() => null); + if (!tokens?.access_token) { + throw new Error('Invalid token response'); + } + + // 3. Fetch user info + const userinfoResponse = await fetchWithTimeout(this.userinfoUrl, { + headers: { Authorization: `Bearer ${tokens.access_token}` }, + }); + + if (!userinfoResponse.ok) { + this.logger.error('Failed to fetch user info'); + throw new Error('Failed to fetch user info'); + } + + const userinfo = await userinfoResponse.json().catch(() => null); + if (!userinfo?.sub) { + throw new Error('Invalid userinfo response'); + } + + // 4. Upsert user in DB + const user = await this.upsertUser(userinfo); + + // 4b. Check if user is banned + try { + const perms = await this.rsvpService.getPerms(userinfo.email); + if (perms === 'Banned') { + return { + token: '', + refreshToken: '', + redirectTo: 'https://fraud.hackclub.com/', + }; + } + } catch (err) { + this.logger.error(`Perms check failed for ${userinfo.sub}: ${err}`); + } + + // 5. Submit RSVP + let redirectTo = '/home'; + try { + const rsvpResult = await this.rsvpService.createRsvp(userinfo.email); + redirectTo = rsvpResult.existing ? '/home' : '/tutorial'; + } catch (err) { + this.logger.error( + `RSVP submission failed for user ${userinfo.sub}: ${err}`, + ); + } + + if (!ALLOWED_REDIRECTS.has(redirectTo)) { + redirectTo = '/home'; + } + + // 6. Create session with refresh token + const refreshToken = await this.createSession(user.id); + + // 7. Sign JWT — no PII beyond what's needed for display + auth checks + const token = this.jwtService.sign({ + sub: userinfo.sub, + uid: user.id, + email: userinfo.email, + name: userinfo.name, + nickname: userinfo.nickname, + slack_id: userinfo.slack_id, + has_address: user.hasAddress, + has_birthdate: user.hasBirthdate, + }); + + return { token, refreshToken, redirectTo }; + } + + /** + * Validates a refresh token, rotates it, and issues a new JWT. + */ + async refreshAuth( + refreshToken: string, + ): Promise<{ token: string; refreshToken: string }> { + const hash = createHash('sha256').update(refreshToken).digest('hex'); + const session = await this.sessionRepo.findOne({ + where: { refreshTokenHash: hash }, + relations: ['user'], + }); + + if (!session || session.expiresAt < new Date()) { + if (session) await this.sessionRepo.remove(session); + throw new Error('Invalid or expired refresh token'); + } + + const user = session.user; + + // Rotate: delete old session, create new one + await this.sessionRepo.remove(session); + const newRefreshToken = await this.createSession(user.id); + + const token = this.jwtService.sign({ + sub: user.hcaSub, + uid: user.id, + email: user.email, + name: user.name, + nickname: user.nickname, + slack_id: user.slackId, + has_address: user.hasAddress, + has_birthdate: user.hasBirthdate, + }); + + return { token, refreshToken: newRefreshToken }; + } + + /** + * Invalidates a refresh token (logout). + */ + async invalidateSession(refreshToken: string): Promise { + const hash = createHash('sha256').update(refreshToken).digest('hex'); + await this.sessionRepo.delete({ refreshTokenHash: hash }); + } + + verifyToken(token: string): Record { + return this.jwtService.verify(token); + } + + /** + * Issues a JWT that lets an admin act as the target user. + * The token carries the target user's identity but includes + * impersonator_uid / impersonator_name so audit logs can attribute actions. + */ + async issueImpersonationToken( + targetUserId: string, + adminUid: string, + adminName: string, + ): Promise<{ token: string }> { + const user = await this.userRepo.findOne({ where: { id: targetUserId } }); + if (!user) throw new Error('User not found'); + + const token = this.jwtService.sign({ + sub: user.hcaSub, + uid: user.id, + email: user.email, + name: user.name, + nickname: user.nickname, + slack_id: user.slackId, + has_address: user.hasAddress, + has_birthdate: user.hasBirthdate, + impersonator_uid: adminUid, + impersonator_name: adminName, + }); + + return { token }; + } + + async updateNickname( + userId: string, + nickname: string, + ): Promise<{ token: string }> { + const user = await this.userRepo.findOne({ where: { id: userId } }); + if (!user) throw new Error('User not found'); + + user.nickname = nickname; + await this.userRepo.save(user); + + const token = this.jwtService.sign({ + sub: user.hcaSub, + uid: user.id, + email: user.email, + name: user.name, + nickname: user.nickname, + slack_id: user.slackId, + has_address: user.hasAddress, + has_birthdate: user.hasBirthdate, + }); + + return { token }; + } + + private async upsertUser(userinfo: Record): Promise { + const hasAddress = !!( + userinfo.address || + (Array.isArray(userinfo.addresses) && userinfo.addresses.length > 0) + ); + const hasBirthdate = !!( + userinfo.birthdate && userinfo.birthdate.trim() !== '' + ); + + let user = await this.userRepo.findOne({ + where: { hcaSub: userinfo.sub }, + }); + + if (user) { + user.email = userinfo.email; + user.name = userinfo.name; + user.nickname = userinfo.nickname; + user.slackId = userinfo.slack_id; + user.hasAddress = hasAddress; + user.hasBirthdate = hasBirthdate; + return this.userRepo.save(user); + } + + // New user — attempt insert. If a concurrent request already inserted + // this hca_sub, catch the unique constraint violation and update instead. + try { + user = this.userRepo.create({ + hcaSub: userinfo.sub, + email: userinfo.email, + name: userinfo.name, + nickname: userinfo.nickname, + slackId: userinfo.slack_id, + hasAddress, + hasBirthdate, + }); + return await this.userRepo.save(user); + } catch (err: any) { + if (err?.code === '23505') { + // Unique violation — the other request won the insert race + user = await this.userRepo.findOne({ + where: { hcaSub: userinfo.sub }, + }); + if (!user) throw err; // shouldn't happen, but safety net + user.email = userinfo.email; + user.name = userinfo.name; + user.nickname = userinfo.nickname; + user.slackId = userinfo.slack_id; + user.hasAddress = hasAddress; + user.hasBirthdate = hasBirthdate; + return this.userRepo.save(user); + } + throw err; + } + } + + private async createSession(userId: string): Promise { + const refreshToken = randomBytes(48).toString('base64url'); + const hash = createHash('sha256').update(refreshToken).digest('hex'); + + const session = this.sessionRepo.create({ + userId, + refreshTokenHash: hash, + expiresAt: new Date(Date.now() + REFRESH_TOKEN_EXPIRY_MS), + }); + await this.sessionRepo.save(session); + + return refreshToken; + } +} diff --git a/backend/src/auth/jwt-auth.guard.ts b/backend/src/auth/jwt-auth.guard.ts new file mode 100644 index 0000000..1c12c1b --- /dev/null +++ b/backend/src/auth/jwt-auth.guard.ts @@ -0,0 +1,29 @@ +import { + CanActivate, + ExecutionContext, + Injectable, + UnauthorizedException, +} from '@nestjs/common'; +import { AuthService } from './auth.service'; + +@Injectable() +export class JwtAuthGuard implements CanActivate { + constructor(private authService: AuthService) {} + + canActivate(context: ExecutionContext): boolean { + const request = context.switchToHttp().getRequest(); + const authHeader = request.headers.authorization; + + if (!authHeader?.startsWith('Bearer ')) { + throw new UnauthorizedException(); + } + + try { + const token = authHeader.split(' ')[1]; + request.user = this.authService.verifyToken(token); + return true; + } catch { + throw new UnauthorizedException(); + } + } +} diff --git a/backend/src/crypto.util.ts b/backend/src/crypto.util.ts new file mode 100644 index 0000000..ada776b --- /dev/null +++ b/backend/src/crypto.util.ts @@ -0,0 +1,58 @@ +import { + createCipheriv, + createDecipheriv, + randomBytes, +} from 'crypto'; +import type { ValueTransformer } from 'typeorm'; + +const ALGO = 'aes-256-gcm'; +const IV_LENGTH = 12; + +export function encrypt(plaintext: string, keyHex: string): string { + const key = Buffer.from(keyHex, 'hex'); + const iv = randomBytes(IV_LENGTH); + const cipher = createCipheriv(ALGO, key, iv); + + const encrypted = Buffer.concat([ + cipher.update(plaintext, 'utf8'), + cipher.final(), + ]); + const tag = cipher.getAuthTag(); + + // iv.tag.ciphertext — all base64url for safe storage + return `${iv.toString('base64')}.${tag.toString('base64')}.${encrypted.toString('base64')}`; +} + +export function decrypt(encoded: string, keyHex: string): string { + const key = Buffer.from(keyHex, 'hex'); + const parts = encoded.split('.'); + if (parts.length !== 3) throw new Error('Malformed encrypted value'); + + const iv = Buffer.from(parts[0], 'base64'); + const tag = Buffer.from(parts[1], 'base64'); + const encrypted = Buffer.from(parts[2], 'base64'); + + const decipher = createDecipheriv(ALGO, key, iv); + decipher.setAuthTag(tag); + + return decipher.update(encrypted).toString('utf8') + decipher.final('utf8'); +} + +/** + * TypeORM column transformer that encrypts on write and decrypts on read. + * Reads DB_ENCRYPTION_KEY from process.env (available after ConfigModule.forRoot). + */ +export const encryptedTransformer: ValueTransformer = { + to(value: string | null): string | null { + if (!value) return value; + const key = process.env.DB_ENCRYPTION_KEY; + if (!key) throw new Error('DB_ENCRYPTION_KEY is not set'); + return encrypt(value, key); + }, + from(value: string | null): string | null { + if (!value) return value; + const key = process.env.DB_ENCRYPTION_KEY; + if (!key) throw new Error('DB_ENCRYPTION_KEY is not set'); + return decrypt(value, key); + }, +}; diff --git a/backend/src/data-source.ts b/backend/src/data-source.ts new file mode 100644 index 0000000..d42ee02 --- /dev/null +++ b/backend/src/data-source.ts @@ -0,0 +1,13 @@ +import 'dotenv/config'; +import { DataSource } from 'typeorm'; + +/** + * Standalone DataSource used by the TypeORM CLI for migrations. + * Reads DATABASE_URL from the environment (loaded via dotenv above). + */ +export default new DataSource({ + type: 'postgres', + url: process.env.DATABASE_URL, + entities: ['src/entities/*.ts'], + migrations: ['src/migrations/*.ts'], +}); diff --git a/backend/src/entities/audit-log.entity.ts b/backend/src/entities/audit-log.entity.ts new file mode 100644 index 0000000..57b53d1 --- /dev/null +++ b/backend/src/entities/audit-log.entity.ts @@ -0,0 +1,48 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + ManyToOne, + JoinColumn, +} from 'typeorm'; +import { User } from './user.entity'; + +export const AUDIT_ACTIONS = [ + 'project_created', + 'project_updated', + 'project_submitted', + 'project_deleted', + 'hackatime_connected', + 'rsvp_submitted', + 'admin_ban', + 'admin_perms_change', + 'project_reviewed', + 'admin_impersonate', + 'shop_purchase', + 'order_fulfilled', +] as const; + +export type AuditAction = (typeof AUDIT_ACTIONS)[number]; + +@Entity('audit_logs') +export class AuditLog { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ name: 'user_id' }) + userId: string; + + @ManyToOne(() => User, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'user_id' }) + user: User; + + @Column({ length: 50 }) + action: string; + + @Column({ length: 255 }) + label: string; + + @CreateDateColumn({ name: 'created_at' }) + createdAt: Date; +} diff --git a/backend/src/entities/comment.entity.ts b/backend/src/entities/comment.entity.ts new file mode 100644 index 0000000..2625f73 --- /dev/null +++ b/backend/src/entities/comment.entity.ts @@ -0,0 +1,36 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + ManyToOne, + JoinColumn, +} from 'typeorm'; +import { Project } from './project.entity'; +import { User } from './user.entity'; + +@Entity('comments') +export class Comment { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ name: 'project_id' }) + projectId: string; + + @ManyToOne(() => Project, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'project_id' }) + project: Project; + + @Column({ name: 'user_id' }) + userId: string; + + @ManyToOne(() => User, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'user_id' }) + user: User; + + @Column({ type: 'varchar', length: 500 }) + body: string; + + @CreateDateColumn({ name: 'created_at' }) + createdAt: Date; +} diff --git a/backend/src/entities/fulfillment-update.entity.ts b/backend/src/entities/fulfillment-update.entity.ts new file mode 100644 index 0000000..bf7ce71 --- /dev/null +++ b/backend/src/entities/fulfillment-update.entity.ts @@ -0,0 +1,39 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + ManyToOne, + JoinColumn, +} from 'typeorm'; +import { User } from './user.entity'; +import { Order } from './order.entity'; + +@Entity('fulfillment_updates') +export class FulfillmentUpdate { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ name: 'user_id' }) + userId: string; + + @ManyToOne(() => User, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'user_id' }) + user: User; + + @Column({ name: 'order_id' }) + orderId: string; + + @ManyToOne(() => Order, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'order_id' }) + order: Order; + + @Column({ length: 500 }) + message: string; + + @Column({ name: 'is_read', default: false }) + isRead: boolean; + + @CreateDateColumn({ name: 'created_at' }) + createdAt: Date; +} diff --git a/backend/src/entities/news-item.entity.ts b/backend/src/entities/news-item.entity.ts new file mode 100644 index 0000000..d834500 --- /dev/null +++ b/backend/src/entities/news-item.entity.ts @@ -0,0 +1,25 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + UpdateDateColumn, +} from 'typeorm'; + +@Entity('news_items') +export class NewsItem { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ length: 500 }) + text: string; + + @Column({ name: 'display_date', type: 'date' }) + displayDate: string; + + @CreateDateColumn({ name: 'created_at' }) + createdAt: Date; + + @UpdateDateColumn({ name: 'updated_at' }) + updatedAt: Date; +} diff --git a/backend/src/entities/order.entity.ts b/backend/src/entities/order.entity.ts new file mode 100644 index 0000000..8ed1d5c --- /dev/null +++ b/backend/src/entities/order.entity.ts @@ -0,0 +1,49 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + UpdateDateColumn, + ManyToOne, + JoinColumn, +} from 'typeorm'; +import { User } from './user.entity'; +import { ShopItem } from './shop-item.entity'; + +@Entity('orders') +export class Order { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ name: 'user_id' }) + userId: string; + + @ManyToOne(() => User, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'user_id' }) + user: User; + + @Column({ name: 'shop_item_id', nullable: true }) + shopItemId: string | null; + + @ManyToOne(() => ShopItem, { onDelete: 'SET NULL', nullable: true }) + @JoinColumn({ name: 'shop_item_id' }) + shopItem: ShopItem; + + @Column({ type: 'integer' }) + quantity: number; + + @Column({ name: 'pipes_spent', type: 'integer' }) + pipesSpent: number; + + @Column({ name: 'item_name', length: 200 }) + itemName: string; + + @Column({ length: 20, default: 'pending' }) + status: string; // 'pending' | 'fulfilled' + + @CreateDateColumn({ name: 'created_at' }) + createdAt: Date; + + @UpdateDateColumn({ name: 'updated_at' }) + updatedAt: Date; +} diff --git a/backend/src/entities/project-review.entity.ts b/backend/src/entities/project-review.entity.ts new file mode 100644 index 0000000..79ca2f0 --- /dev/null +++ b/backend/src/entities/project-review.entity.ts @@ -0,0 +1,53 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + ManyToOne, + JoinColumn, +} from 'typeorm'; +import { Project } from './project.entity'; +import { Submission } from './submission.entity'; +import { User } from './user.entity'; + +@Entity('project_reviews') +export class ProjectReview { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ name: 'project_id' }) + projectId: string; + + @ManyToOne(() => Project, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'project_id' }) + project: Project; + + @Column({ name: 'reviewer_id' }) + reviewerId: string; + + @ManyToOne(() => User, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'reviewer_id' }) + reviewer: User; + + @Column({ name: 'submission_id', nullable: true }) + submissionId: string | null; + + @ManyToOne(() => Submission, { onDelete: 'CASCADE', nullable: true }) + @JoinColumn({ name: 'submission_id' }) + submission: Submission; + + @Column({ length: 20 }) + status: string; + + @Column({ type: 'text', nullable: true }) + feedback: string | null; + + @Column({ type: 'text', name: 'internal_note', nullable: true }) + internalNote: string | null; + + @Column({ type: 'text', name: 'override_justification', nullable: true }) + overrideJustification: string | null; + + @CreateDateColumn({ name: 'created_at' }) + createdAt: Date; +} diff --git a/backend/src/entities/project.entity.ts b/backend/src/entities/project.entity.ts new file mode 100644 index 0000000..85a2b21 --- /dev/null +++ b/backend/src/entities/project.entity.ts @@ -0,0 +1,106 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + UpdateDateColumn, + ManyToOne, + JoinColumn, +} from 'typeorm'; +import { User } from './user.entity'; + +export const VALID_PROJECT_STATUSES = [ + 'unshipped', + 'unreviewed', + 'changes_needed', + 'approved', +] as const; + +export type ProjectStatus = (typeof VALID_PROJECT_STATUSES)[number]; + +export const VALID_PROJECT_TYPES = [ + 'web', + 'windows', + 'mac', + 'linux', + 'cross-platform', + 'python', + 'android', + 'ios', +] as const; + +export type ProjectType = (typeof VALID_PROJECT_TYPES)[number]; + +@Entity('projects') +export class Project { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ name: 'user_id' }) + userId: string; + + @ManyToOne(() => User, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'user_id' }) + user: User; + + @Column({ length: 50 }) + name: string; + + @Column({ length: 300 }) + description: string; + + @Column({ name: 'project_type', length: 20 }) + projectType: string; + + @Column({ type: 'varchar', name: 'code_url', length: 2048, nullable: true }) + codeUrl: string | null; + + @Column({ type: 'varchar', name: 'readme_url', length: 2048, nullable: true }) + readmeUrl: string | null; + + @Column({ type: 'varchar', name: 'demo_url', length: 2048, nullable: true }) + demoUrl: string | null; + + @Column({ type: 'varchar', name: 'screenshot_1_url', length: 2048, nullable: true }) + screenshot1Url: string | null; + + @Column({ type: 'varchar', name: 'screenshot_2_url', length: 2048, nullable: true }) + screenshot2Url: string | null; + + @Column({ type: 'text', name: 'hackatime_project_name', nullable: true, transformer: { + to: (value: string[] | null) => value && value.length > 0 ? JSON.stringify(value) : null, + from: (value: string | null) => { + if (!value) return []; + try { const parsed = JSON.parse(value); return Array.isArray(parsed) ? parsed : [value]; } + catch { return [value]; } + }, + }}) + hackatimeProjectName: string[]; + + @Column({ name: 'status', length: 20, default: 'unshipped' }) + status: string; + + @Column({ type: 'real', name: 'override_hours', nullable: true }) + overrideHours: number | null; + + @Column({ type: 'real', name: 'internal_hours', nullable: true }) + internalHours: number | null; + + @Column({ name: 'is_update', default: false }) + isUpdate: boolean; + + @Column({ type: 'varchar', name: 'other_hc_program', length: 255, nullable: true }) + otherHcProgram: string | null; + + @Column({ type: 'varchar', name: 'ai_use', length: 200, nullable: true }) + aiUse: string | null; + + @Column({ type: 'integer', name: 'pipes_granted', default: 0 }) + pipesGranted: number; + + @CreateDateColumn({ name: 'created_at' }) + createdAt: Date; + + @UpdateDateColumn({ name: 'updated_at' }) + updatedAt: Date; +} diff --git a/backend/src/entities/session.entity.ts b/backend/src/entities/session.entity.ts new file mode 100644 index 0000000..06a8231 --- /dev/null +++ b/backend/src/entities/session.entity.ts @@ -0,0 +1,31 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + ManyToOne, + JoinColumn, +} from 'typeorm'; +import { User } from './user.entity'; + +@Entity('sessions') +export class Session { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ name: 'user_id' }) + userId: string; + + @ManyToOne(() => User, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'user_id' }) + user: User; + + @Column({ unique: true, name: 'refresh_token_hash' }) + refreshTokenHash: string; + + @Column({ name: 'expires_at' }) + expiresAt: Date; + + @CreateDateColumn({ name: 'created_at' }) + createdAt: Date; +} diff --git a/backend/src/entities/shop-item.entity.ts b/backend/src/entities/shop-item.entity.ts new file mode 100644 index 0000000..6299676 --- /dev/null +++ b/backend/src/entities/shop-item.entity.ts @@ -0,0 +1,43 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + UpdateDateColumn, +} from 'typeorm'; + +@Entity('shop_items') +export class ShopItem { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ length: 200 }) + name: string; + + @Column({ length: 500 }) + description: string; + + @Column({ name: 'image_url', length: 500 }) + imageUrl: string; + + @Column({ name: 'price_hours', type: 'integer' }) + priceHours: number; + + @Column({ type: 'integer', nullable: true, default: null }) + stock: number | null; + + @Column({ name: 'sort_order', type: 'integer', default: 0 }) + sortOrder: number; + + @Column({ name: 'is_active', type: 'boolean', default: true }) + isActive: boolean; + + @Column({ name: 'estimated_ship', type: 'varchar', length: 200, nullable: true, default: null }) + estimatedShip: string | null; + + @CreateDateColumn({ name: 'created_at' }) + createdAt: Date; + + @UpdateDateColumn({ name: 'updated_at' }) + updatedAt: Date; +} diff --git a/backend/src/entities/submission.entity.ts b/backend/src/entities/submission.entity.ts new file mode 100644 index 0000000..33e4a16 --- /dev/null +++ b/backend/src/entities/submission.entity.ts @@ -0,0 +1,51 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + ManyToOne, + JoinColumn, +} from 'typeorm'; +import { Project } from './project.entity'; +import { User } from './user.entity'; + +@Entity('submissions') +export class Submission { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ name: 'project_id' }) + projectId: string; + + @ManyToOne(() => Project, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'project_id' }) + project: Project; + + @Column({ name: 'user_id' }) + userId: string; + + @ManyToOne(() => User, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'user_id' }) + user: User; + + @Column({ type: 'text', name: 'change_description', nullable: true }) + changeDescription: string | null; + + @Column({ name: 'min_hours_confirmed', default: false }) + minHoursConfirmed: boolean; + + @Column({ length: 20, default: 'unreviewed' }) + status: string; // 'unreviewed' | 'approved' | 'changes_needed' + + @Column({ type: 'real', name: 'override_hours', nullable: true }) + overrideHours: number | null; + + @Column({ type: 'real', name: 'internal_hours', nullable: true }) + internalHours: number | null; + + @Column({ type: 'integer', name: 'pipes_granted', default: 0 }) + pipesGranted: number; + + @CreateDateColumn({ name: 'created_at' }) + createdAt: Date; +} diff --git a/backend/src/entities/user.entity.ts b/backend/src/entities/user.entity.ts new file mode 100644 index 0000000..5618a83 --- /dev/null +++ b/backend/src/entities/user.entity.ts @@ -0,0 +1,58 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + UpdateDateColumn, +} from 'typeorm'; +import { encryptedTransformer } from '../crypto.util'; + +@Entity('users') +export class User { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ unique: true, name: 'hca_sub' }) + hcaSub: string; + + @Column({ type: 'text', transformer: encryptedTransformer }) + email: string; + + @Column({ nullable: true }) + name: string; + + @Column({ nullable: true }) + nickname: string; + + @Column({ nullable: true, name: 'slack_id' }) + slackId: string; + + @Column({ name: 'two_emails', default: false }) + twoEmails: boolean; + + @Column({ name: 'has_address', default: false }) + hasAddress: boolean; + + @Column({ name: 'has_birthdate', default: false }) + hasBirthdate: boolean; + + @Column({ nullable: true, name: 'hackatime_user_id' }) + hackatimeUserId: string; + + @Column({ + nullable: true, + name: 'hackatime_token', + type: 'text', + transformer: encryptedTransformer, + }) + hackatimeToken: string; + + @Column({ type: 'integer', default: 0 }) + pipes: number; + + @CreateDateColumn({ name: 'created_at' }) + createdAt: Date; + + @UpdateDateColumn({ name: 'updated_at' }) + updatedAt: Date; +} diff --git a/backend/src/fetch.util.ts b/backend/src/fetch.util.ts new file mode 100644 index 0000000..3e1630f --- /dev/null +++ b/backend/src/fetch.util.ts @@ -0,0 +1,16 @@ +/** + * Wrapper around fetch with a default timeout. + * Prevents hanging requests to external services from blocking the server. + */ +export function fetchWithTimeout( + url: string | URL, + init?: RequestInit & { timeoutMs?: number }, +): Promise { + const { timeoutMs = 10000, ...fetchInit } = init ?? {}; + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), timeoutMs); + + return fetch(url, { ...fetchInit, signal: controller.signal }).finally(() => + clearTimeout(timer), + ); +} diff --git a/backend/src/hackatime/hackatime.controller.ts b/backend/src/hackatime/hackatime.controller.ts new file mode 100644 index 0000000..6142b07 --- /dev/null +++ b/backend/src/hackatime/hackatime.controller.ts @@ -0,0 +1,87 @@ +import { + Controller, + Post, + Get, + Body, + Req, + UseGuards, + UnauthorizedException, + BadRequestException, +} from '@nestjs/common'; +import { Throttle } from '@nestjs/throttler'; +import type { Request } from 'express'; +import { HackatimeService } from './hackatime.service'; +import { JwtAuthGuard } from '../auth/jwt-auth.guard'; + +@Controller('api/hackatime') +export class HackatimeController { + constructor(private readonly hackatimeService: HackatimeService) {} + + /** + * Generates Hackatime OAuth state and authorization URL. + * Requires an authenticated user (must be logged in via HCA first). + */ + @Throttle({ default: { limit: 10, ttl: 60000 } }) + @UseGuards(JwtAuthGuard) + @Post('start') + start() { + return this.hackatimeService.startAuth(); + } + + /** + * Handles the Hackatime OAuth callback: state verification, code exchange, + * and storing the connection for the authenticated user. + */ + @Throttle({ default: { limit: 10, ttl: 60000 } }) + @UseGuards(JwtAuthGuard) + @Post('callback') + async handleCallback( + @Req() req: Request, + @Body() + body: { + code: string; + state: string; + storedState: string; + }, + ) { + if (!body.code) { + throw new BadRequestException('Authorization code is required'); + } + if (!body.state || !body.storedState) { + throw new BadRequestException('State parameters are required'); + } + + const user = (req as any).user; + const userId = user?.sub; + if (!userId) { + throw new UnauthorizedException('No user identity'); + } + + try { + return await this.hackatimeService.handleCallback( + body.code, + body.state, + body.storedState, + userId, + user.impersonator_name, + ); + } catch { + throw new UnauthorizedException('Hackatime authentication failed'); + } + } + + /** + * Returns the authenticated user's Hackatime project names. + * Only project name strings are returned — no other Hackatime data. + */ + @Throttle({ default: { limit: 15, ttl: 60000 } }) + @UseGuards(JwtAuthGuard) + @Get('projects') + async getProjects(@Req() req: Request) { + const userId = (req as any).user?.sub; + if (!userId) throw new UnauthorizedException('No user identity'); + + const names = await this.hackatimeService.getProjectNames(userId); + return { projects: names }; + } +} diff --git a/backend/src/hackatime/hackatime.module.ts b/backend/src/hackatime/hackatime.module.ts new file mode 100644 index 0000000..b3c1e70 --- /dev/null +++ b/backend/src/hackatime/hackatime.module.ts @@ -0,0 +1,17 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { AuthModule } from '../auth/auth.module'; +import { AuditLogModule } from '../audit-log/audit-log.module'; +import { RsvpModule } from '../rsvp/rsvp.module'; +import { User } from '../entities/user.entity'; +import { Session } from '../entities/session.entity'; +import { HackatimeService } from './hackatime.service'; +import { HackatimeController } from './hackatime.controller'; + +@Module({ + imports: [AuthModule, AuditLogModule, RsvpModule, TypeOrmModule.forFeature([User, Session])], + controllers: [HackatimeController], + providers: [HackatimeService], + exports: [HackatimeService], +}) +export class HackatimeModule {} diff --git a/backend/src/hackatime/hackatime.service.ts b/backend/src/hackatime/hackatime.service.ts new file mode 100644 index 0000000..a115e94 --- /dev/null +++ b/backend/src/hackatime/hackatime.service.ts @@ -0,0 +1,346 @@ +import { Injectable, Logger, OnModuleInit } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { createHmac, timingSafeEqual } from 'crypto'; +import { IsNull } from 'typeorm'; +import { fetchWithTimeout } from '../fetch.util'; +import { User } from '../entities/user.entity'; +import { Session } from '../entities/session.entity'; +import { AuditLogService } from '../audit-log/audit-log.service'; +import { RsvpService } from '../rsvp/rsvp.service'; + +@Injectable() +export class HackatimeService implements OnModuleInit { + private readonly logger = new Logger(HackatimeService.name); + private readonly clientId: string | undefined; + private readonly clientSecret: string | undefined; + private readonly redirectUri: string; + private readonly jwtSecret: string; + private readonly baseUrl: string; + private readonly configured: boolean; + + constructor( + private configService: ConfigService, + @InjectRepository(User) + private userRepo: Repository, + @InjectRepository(Session) + private sessionRepo: Repository, + private auditLogService: AuditLogService, + private rsvpService: RsvpService, + ) { + this.clientId = this.configService.get('HACKATIME_CLIENT_ID'); + this.clientSecret = this.configService.get('HACKATIME_CLIENT_SECRET'); + this.redirectUri = this.configService.get( + 'HACKATIME_REDIRECT_URI', + 'http://localhost:5173/auth/hackatime/callback', + ); + this.jwtSecret = this.configService.getOrThrow('JWT_SECRET'); + this.baseUrl = this.configService.get( + 'HACKATIME_BASE_URL', + 'https://hackatime.hackclub.com', + ); + this.configured = !!(this.clientId && this.clientSecret); + if (!this.configured) { + this.logger.warn('HACKATIME_CLIENT_ID/SECRET not set — Hackatime OAuth disabled'); + } + } + + private assertConfigured(): void { + if (!this.configured) { + throw new Error('Hackatime OAuth is not configured'); + } + } + + private signState(state: string): string { + // Prefix with flow name to prevent cross-flow state confusion with HCA OAuth + return createHmac('sha256', this.jwtSecret) + .update(`hackatime:${state}`) + .digest('hex'); + } + + startAuth(): { url: string; state: string } { + this.assertConfigured(); + + const state = crypto.randomUUID(); + const signature = this.signState(state); + const signedState = `${state}.${signature}`; + + const params = new URLSearchParams({ + client_id: this.clientId!, + redirect_uri: this.redirectUri, + response_type: 'code', + scope: 'profile read', + state: signedState, + }); + + return { + url: `${this.baseUrl}/oauth/authorize?${params.toString()}`, + state, + }; + } + + async handleCallback( + code: string, + returnedSignedState: string, + cookieState: string, + userId: string, + impersonatorName?: string, + ): Promise<{ success: boolean; redirectTo: string }> { + this.assertConfigured(); + + // 1. Verify state (same HMAC pattern as HCA OAuth) + const dotIndex = returnedSignedState.lastIndexOf('.'); + if (dotIndex === -1) { + throw new Error('Malformed state parameter'); + } + + const stateValue = returnedSignedState.substring(0, dotIndex); + const signature = returnedSignedState.substring(dotIndex + 1); + + const stateBuffer = Buffer.from(stateValue); + const cookieBuffer = Buffer.from(cookieState); + if ( + stateBuffer.length !== cookieBuffer.length || + !timingSafeEqual(stateBuffer, cookieBuffer) + ) { + throw new Error('State mismatch'); + } + + const expectedSignature = this.signState(stateValue); + const sigBuffer = Buffer.from(signature); + const expectedBuffer = Buffer.from(expectedSignature); + if ( + sigBuffer.length !== expectedBuffer.length || + !timingSafeEqual(sigBuffer, expectedBuffer) + ) { + throw new Error('Invalid state signature'); + } + + // 2. Exchange code for tokens + const tokenResponse = await fetchWithTimeout(`${this.baseUrl}/oauth/token`, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: new URLSearchParams({ + grant_type: 'authorization_code', + code, + redirect_uri: this.redirectUri, + client_id: this.clientId!, + client_secret: this.clientSecret!, + }), + }); + + if (!tokenResponse.ok) { + this.logger.error( + `Hackatime token exchange failed: ${tokenResponse.status}`, + ); + throw new Error('Hackatime token exchange failed'); + } + + const tokens = await tokenResponse.json().catch(() => null); + + if (!tokens?.access_token) { + this.logger.error('Hackatime token response missing or malformed'); + throw new Error('Invalid token response from Hackatime'); + } + + // 3. Check if the user is banned on Hackatime + grab their Hackatime user ID + let hackatimeUid: string | null = null; + try { + const meRes = await fetchWithTimeout( + `${this.baseUrl}/api/v1/authenticated/me`, + { headers: { Authorization: `Bearer ${tokens.access_token}` } }, + ); + if (meRes.ok) { + const meData = await meRes.json(); + const d = meData?.data ?? meData; + hackatimeUid = d?.id?.toString() ?? d?.user_id?.toString() ?? null; + const trustData = d?.trust_factor ?? meData?.trust_factor; + if (trustData?.trust_level === 'red') { + this.logger.warn(`Hackatime-banned user attempted connection: ${userId}`); + const user = await this.userRepo.findOne({ where: { hcaSub: userId } }); + if (user?.email) { + await this.rsvpService.updatePerms(user.email, 'Banned'); + await this.sessionRepo.delete({ userId: user.id }); + } + return { success: false, redirectTo: 'https://fraud.hackclub.com/' }; + } + } + } catch (err) { + this.logger.error(`Hackatime ban check failed for ${userId}: ${err}`); + } + + // 4. Persist the token (and Hackatime user ID) to the user's DB record + // Use find+save (not update) so the column encryption transformer runs + const user = await this.userRepo.findOne({ where: { hcaSub: userId } }); + if (!user) { + throw new Error('User not found'); + } + user.hackatimeToken = tokens.access_token; + if (hackatimeUid) { + user.hackatimeUserId = hackatimeUid; + } + await this.userRepo.save(user); + this.logger.log(`Hackatime connected for user ${userId}`); + + await this.auditLogService.log(user.id, 'hackatime_connected', 'Connected Hackatime', impersonatorName); + + return { success: true, redirectTo: '/tutorial?stage=2' }; + } + + async isConnected(userId: string): Promise { + const user = await this.userRepo.findOne({ + where: { hcaSub: userId }, + select: ['hackatimeToken'], + }); + return !!user?.hackatimeToken; + } + + /** + * Fetches the authenticated user's Hackatime project names. + * Returns only the project name strings — no other data is exposed. + */ + async getProjectNames(userId: string): Promise { + const user = await this.userRepo.findOne({ + where: { hcaSub: userId }, + select: ['hackatimeToken'], + }); + + if (!user?.hackatimeToken) { + this.logger.warn(`No hackatime token found for user ${userId} (user found: ${!!user})`); + return []; + } + + try { + const res = await fetchWithTimeout( + `${this.baseUrl}/api/v1/authenticated/projects`, + { + headers: { Authorization: `Bearer ${user.hackatimeToken}` }, + }, + ); + + if (!res.ok) { + this.logger.warn( + `Hackatime projects fetch failed (${res.status}) for user ${userId}`, + ); + return []; + } + + const data = await res.json(); + const projects: { name: string }[] = data?.projects ?? data?.data ?? []; + + if (!Array.isArray(projects)) return []; + + return projects + .map((p) => (typeof p === 'string' ? p : p?.name)) + .filter((n): n is string => typeof n === 'string' && n.length > 0); + } catch (err) { + this.logger.error(`Hackatime projects fetch error for ${userId}: ${err}`); + return []; + } + } + + /** + * Fetches all-time stats from Hackatime and returns total hours + * plus a per-project-name breakdown for the specified project names. + * Single API call — no duplication. + */ + async getHoursForProjects( + userId: string, + projectNames: string[], + ): Promise<{ hours: number; perProject: Record }> { + if (projectNames.length === 0) { + return { hours: 0, perProject: {} }; + } + + const user = await this.userRepo.findOne({ + where: { hcaSub: userId }, + select: ['hackatimeToken'], + }); + + if (!user?.hackatimeToken) { + return { hours: 0, perProject: {} }; + } + + try { + const res = await fetchWithTimeout( + `${this.baseUrl}/api/v1/authenticated/projects`, + { + headers: { Authorization: `Bearer ${user.hackatimeToken}` }, + }, + ); + + if (!res.ok) { + this.logger.warn( + `Hackatime stats fetch failed (${res.status}) for user ${userId}`, + ); + return { hours: 0, perProject: {} }; + } + + const body = await res.json().catch(() => null); + const projects: { name: string; total_seconds: number }[] = + body?.projects ?? body?.data ?? []; + + if (!Array.isArray(projects)) { + return { hours: 0, perProject: {} }; + } + + const nameSet = new Set(projectNames); + let totalSeconds = 0; + const perProject: Record = {}; + + for (const p of projects) { + if (nameSet.has(p.name)) { + const secs = p.total_seconds ?? 0; + totalSeconds += secs; + perProject[p.name] = Math.round((secs / 3600) * 10) / 10; + } + } + + return { + hours: Math.round((totalSeconds / 3600) * 10) / 10, + perProject, + }; + } catch (err) { + this.logger.error(`Hackatime stats fetch error for ${userId}: ${err}`); + return { hours: 0, perProject: {} }; + } + } + + /** + * One-time backfill: for users who connected Hackatime before we started + * storing the Hackatime user ID, fetch it via their stored OAuth token. + */ + async onModuleInit() { + const needsBackfill = await this.userRepo.find({ + where: { hackatimeUserId: IsNull() }, + select: ['id', 'hcaSub', 'hackatimeToken', 'hackatimeUserId'], + }).then((users) => users.filter((u) => !!u.hackatimeToken)); + if (needsBackfill.length === 0) return; + + this.logger.log(`Backfilling Hackatime user IDs for ${needsBackfill.length} user(s)...`); + + for (const user of needsBackfill) { + try { + const res = await fetchWithTimeout( + `${this.baseUrl}/api/v1/authenticated/me`, + { headers: { Authorization: `Bearer ${user.hackatimeToken}` } }, + ); + if (!res.ok) { + this.logger.warn(`Backfill: /me failed (${res.status}) for user ${user.hcaSub}`); + continue; + } + const raw = await res.json(); + const data = raw?.data ?? raw; + const htId = data?.id?.toString() ?? data?.user_id?.toString() ?? null; + if (htId) { + user.hackatimeUserId = htId; + await this.userRepo.save(user); + this.logger.log(`Backfill: stored Hackatime user ID ${htId} for user ${user.hcaSub}`); + } + } catch (err) { + this.logger.warn(`Backfill: error for user ${user.hcaSub}: ${err}`); + } + } + } +} diff --git a/backend/src/health.controller.ts b/backend/src/health.controller.ts new file mode 100644 index 0000000..3b6fb40 --- /dev/null +++ b/backend/src/health.controller.ts @@ -0,0 +1,9 @@ +import { Controller, Get } from '@nestjs/common'; + +@Controller('api/health') +export class HealthController { + @Get() + check() { + return { status: 'ok' }; + } +} diff --git a/backend/src/main.ts b/backend/src/main.ts new file mode 100644 index 0000000..984deac --- /dev/null +++ b/backend/src/main.ts @@ -0,0 +1,26 @@ +import { NestFactory } from '@nestjs/core'; +import { Logger } from '@nestjs/common'; +import { json } from 'express'; +import helmet from 'helmet'; +import { AppModule } from './app.module'; + +process.on('unhandledRejection', (reason) => { + const logger = new Logger('UnhandledRejection'); + logger.fatal('Unhandled promise rejection', reason); + process.exit(1); +}); + +async function bootstrap() { + const app = await NestFactory.create(AppModule); + + app.use(json({ limit: '50mb' })); + app.use(helmet()); + + app.enableCors({ + origin: process.env.FRONTEND_URL ?? 'http://localhost:5173', + credentials: true, + }); + + await app.listen(process.env.PORT ?? 3001); +} +bootstrap(); diff --git a/backend/src/migrations/1743552000000-InitialSchema.ts b/backend/src/migrations/1743552000000-InitialSchema.ts new file mode 100644 index 0000000..cdf9893 --- /dev/null +++ b/backend/src/migrations/1743552000000-InitialSchema.ts @@ -0,0 +1,67 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class InitialSchema1743552000000 implements MigrationInterface { + name = 'InitialSchema1743552000000'; + + public async up(queryRunner: QueryRunner): Promise { + // — users — + await queryRunner.query(` + CREATE TABLE IF NOT EXISTS "users" ( + "id" uuid DEFAULT gen_random_uuid() NOT NULL, + "hca_sub" varchar NOT NULL, + "email" text NOT NULL, + "name" varchar, + "nickname" varchar, + "slack_id" varchar, + "hackatime_token" text, + "created_at" TIMESTAMP NOT NULL DEFAULT now(), + "updated_at" TIMESTAMP NOT NULL DEFAULT now(), + CONSTRAINT "PK_users" PRIMARY KEY ("id"), + CONSTRAINT "UQ_users_hca_sub" UNIQUE ("hca_sub") + ) + `); + + // — sessions — + await queryRunner.query(` + CREATE TABLE IF NOT EXISTS "sessions" ( + "id" uuid DEFAULT gen_random_uuid() NOT NULL, + "user_id" uuid NOT NULL, + "refresh_token_hash" varchar NOT NULL, + "expires_at" TIMESTAMP NOT NULL, + "created_at" TIMESTAMP NOT NULL DEFAULT now(), + CONSTRAINT "PK_sessions" PRIMARY KEY ("id"), + CONSTRAINT "UQ_sessions_refresh_token_hash" UNIQUE ("refresh_token_hash"), + CONSTRAINT "FK_sessions_user" FOREIGN KEY ("user_id") + REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE NO ACTION + ) + `); + + // — projects — + await queryRunner.query(` + CREATE TABLE IF NOT EXISTS "projects" ( + "id" uuid DEFAULT gen_random_uuid() NOT NULL, + "user_id" uuid NOT NULL, + "name" varchar(50) NOT NULL, + "description" varchar(300) NOT NULL, + "project_type" varchar(20) NOT NULL, + "code_url" varchar(2048), + "readme_url" varchar(2048), + "demo_url" varchar(2048), + "screenshot_1_url" varchar(2048), + "screenshot_2_url" varchar(2048), + "hackatime_project_name" varchar(255), + "created_at" TIMESTAMP NOT NULL DEFAULT now(), + "updated_at" TIMESTAMP NOT NULL DEFAULT now(), + CONSTRAINT "PK_projects" PRIMARY KEY ("id"), + CONSTRAINT "FK_projects_user" FOREIGN KEY ("user_id") + REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE NO ACTION + ) + `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP TABLE IF EXISTS "projects"`); + await queryRunner.query(`DROP TABLE IF EXISTS "sessions"`); + await queryRunner.query(`DROP TABLE IF EXISTS "users"`); + } +} diff --git a/backend/src/migrations/1743552001000-AddForeignKeyIndexes.ts b/backend/src/migrations/1743552001000-AddForeignKeyIndexes.ts new file mode 100644 index 0000000..eb23a70 --- /dev/null +++ b/backend/src/migrations/1743552001000-AddForeignKeyIndexes.ts @@ -0,0 +1,19 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddForeignKeyIndexes1743552001000 implements MigrationInterface { + name = 'AddForeignKeyIndexes1743552001000'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE INDEX IF NOT EXISTS "idx_sessions_user_id" ON "sessions" ("user_id")`, + ); + await queryRunner.query( + `CREATE INDEX IF NOT EXISTS "idx_projects_user_id" ON "projects" ("user_id")`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP INDEX IF EXISTS "idx_projects_user_id"`); + await queryRunner.query(`DROP INDEX IF EXISTS "idx_sessions_user_id"`); + } +} diff --git a/backend/src/migrations/1743552002000-CreateAuditLogs.ts b/backend/src/migrations/1743552002000-CreateAuditLogs.ts new file mode 100644 index 0000000..ed60614 --- /dev/null +++ b/backend/src/migrations/1743552002000-CreateAuditLogs.ts @@ -0,0 +1,28 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class CreateAuditLogs1743552002000 implements MigrationInterface { + name = 'CreateAuditLogs1743552002000'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + CREATE TABLE IF NOT EXISTS "audit_logs" ( + "id" uuid DEFAULT gen_random_uuid() NOT NULL, + "user_id" uuid NOT NULL, + "action" varchar(50) NOT NULL, + "label" varchar(255) NOT NULL, + "created_at" TIMESTAMP NOT NULL DEFAULT now(), + CONSTRAINT "PK_audit_logs" PRIMARY KEY ("id"), + CONSTRAINT "FK_audit_logs_user" FOREIGN KEY ("user_id") + REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE NO ACTION + ) + `); + + await queryRunner.query( + `CREATE INDEX IF NOT EXISTS "idx_audit_logs_user_id" ON "audit_logs" ("user_id")`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP TABLE IF EXISTS "audit_logs"`); + } +} diff --git a/backend/src/migrations/1775167703252-AddProjectFlags.ts b/backend/src/migrations/1775167703252-AddProjectFlags.ts new file mode 100644 index 0000000..aa72ae1 --- /dev/null +++ b/backend/src/migrations/1775167703252-AddProjectFlags.ts @@ -0,0 +1,26 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class AddProjectFlags1775167703252 implements MigrationInterface { + name = 'AddProjectFlags1775167703252' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "projects" DROP CONSTRAINT "FK_projects_user"`); + await queryRunner.query(`ALTER TABLE "audit_logs" DROP CONSTRAINT "FK_audit_logs_user"`); + await queryRunner.query(`DROP INDEX "public"."idx_sessions_user_id"`); + await queryRunner.query(`DROP INDEX "public"."idx_projects_user_id"`); + await queryRunner.query(`DROP INDEX "public"."idx_audit_logs_user_id"`); + await queryRunner.query(`ALTER TABLE "projects" ADD CONSTRAINT "FK_bd55b203eb9f92b0c8390380010" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "audit_logs" ADD CONSTRAINT "FK_bd2726fd31b35443f2245b93ba0" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "audit_logs" DROP CONSTRAINT "FK_bd2726fd31b35443f2245b93ba0"`); + await queryRunner.query(`ALTER TABLE "projects" DROP CONSTRAINT "FK_bd55b203eb9f92b0c8390380010"`); + await queryRunner.query(`CREATE INDEX "idx_audit_logs_user_id" ON "audit_logs" ("user_id") `); + await queryRunner.query(`CREATE INDEX "idx_projects_user_id" ON "projects" ("user_id") `); + await queryRunner.query(`CREATE INDEX "idx_sessions_user_id" ON "sessions" ("user_id") `); + await queryRunner.query(`ALTER TABLE "audit_logs" ADD CONSTRAINT "FK_audit_logs_user" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "projects" ADD CONSTRAINT "FK_projects_user" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + } + +} diff --git a/backend/src/migrations/1775169229562-AddProjectStatus.ts b/backend/src/migrations/1775169229562-AddProjectStatus.ts new file mode 100644 index 0000000..5702864 --- /dev/null +++ b/backend/src/migrations/1775169229562-AddProjectStatus.ts @@ -0,0 +1,14 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class AddProjectStatus1775169229562 implements MigrationInterface { + name = 'AddProjectStatus1775169229562' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "projects" ADD "status" character varying(20) NOT NULL DEFAULT 'unshipped'`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "projects" DROP COLUMN "status"`); + } + +} diff --git a/backend/src/migrations/1775200000000-AddTwoEmails.ts b/backend/src/migrations/1775200000000-AddTwoEmails.ts new file mode 100644 index 0000000..df075cf --- /dev/null +++ b/backend/src/migrations/1775200000000-AddTwoEmails.ts @@ -0,0 +1,14 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class AddTwoEmails1775200000000 implements MigrationInterface { + name = 'AddTwoEmails1775200000000' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "users" ADD "two_emails" boolean NOT NULL DEFAULT false`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "users" DROP COLUMN "two_emails"`); + } + +} diff --git a/backend/src/migrations/1775300000000-AddAiUse.ts b/backend/src/migrations/1775300000000-AddAiUse.ts new file mode 100644 index 0000000..5cb6811 --- /dev/null +++ b/backend/src/migrations/1775300000000-AddAiUse.ts @@ -0,0 +1,13 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class AddAiUse1775300000000 implements MigrationInterface { + name = 'AddAiUse1775300000000' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "projects" ADD COLUMN IF NOT EXISTS "ai_use" varchar(1000)`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "projects" DROP COLUMN "ai_use"`); + } +} diff --git a/backend/src/migrations/1775300001000-ShrinkAiUse.ts b/backend/src/migrations/1775300001000-ShrinkAiUse.ts new file mode 100644 index 0000000..222f403 --- /dev/null +++ b/backend/src/migrations/1775300001000-ShrinkAiUse.ts @@ -0,0 +1,13 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class ShrinkAiUse1775300001000 implements MigrationInterface { + name = 'ShrinkAiUse1775300001000' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "projects" ALTER COLUMN "ai_use" TYPE varchar(200)`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "projects" ALTER COLUMN "ai_use" TYPE varchar(1000)`); + } +} diff --git a/backend/src/migrations/1775400000000-CreateNewsItems.ts b/backend/src/migrations/1775400000000-CreateNewsItems.ts new file mode 100644 index 0000000..00a0d17 --- /dev/null +++ b/backend/src/migrations/1775400000000-CreateNewsItems.ts @@ -0,0 +1,22 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class CreateNewsItems1775400000000 implements MigrationInterface { + name = 'CreateNewsItems1775400000000' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + CREATE TABLE "news_items" ( + "id" uuid NOT NULL DEFAULT uuid_generate_v4(), + "text" varchar(500) NOT NULL, + "display_date" date NOT NULL, + "created_at" TIMESTAMP NOT NULL DEFAULT now(), + "updated_at" TIMESTAMP NOT NULL DEFAULT now(), + CONSTRAINT "PK_news_items" PRIMARY KEY ("id") + ) + `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP TABLE "news_items"`); + } +} diff --git a/backend/src/migrations/1775500000000-AddHackatimeUserId.ts b/backend/src/migrations/1775500000000-AddHackatimeUserId.ts new file mode 100644 index 0000000..a949eb8 --- /dev/null +++ b/backend/src/migrations/1775500000000-AddHackatimeUserId.ts @@ -0,0 +1,17 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class AddHackatimeUserId1775500000000 implements MigrationInterface { + name = 'AddHackatimeUserId1775500000000' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "users" ADD "hackatime_user_id" varchar NULL`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "users" DROP COLUMN "hackatime_user_id"`, + ); + } +} diff --git a/backend/src/migrations/1775500001000-CreateProjectReviews.ts b/backend/src/migrations/1775500001000-CreateProjectReviews.ts new file mode 100644 index 0000000..3ae4339 --- /dev/null +++ b/backend/src/migrations/1775500001000-CreateProjectReviews.ts @@ -0,0 +1,28 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class CreateProjectReviews1775500001000 implements MigrationInterface { + name = 'CreateProjectReviews1775500001000' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + CREATE TABLE "project_reviews" ( + "id" uuid NOT NULL DEFAULT uuid_generate_v4(), + "project_id" uuid NOT NULL, + "reviewer_id" uuid NOT NULL, + "status" varchar(20) NOT NULL, + "feedback" text, + "internal_note" text, + "override_justification" text, + "created_at" TIMESTAMP NOT NULL DEFAULT now(), + CONSTRAINT "PK_project_reviews" PRIMARY KEY ("id"), + CONSTRAINT "FK_project_reviews_project" FOREIGN KEY ("project_id") REFERENCES "projects"("id") ON DELETE CASCADE, + CONSTRAINT "FK_project_reviews_reviewer" FOREIGN KEY ("reviewer_id") REFERENCES "users"("id") ON DELETE CASCADE + ) + `); + await queryRunner.query(`CREATE INDEX "IDX_project_reviews_project_id" ON "project_reviews" ("project_id")`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP TABLE "project_reviews"`); + } +} diff --git a/backend/src/migrations/1775500002000-AddProjectHoursOverrides.ts b/backend/src/migrations/1775500002000-AddProjectHoursOverrides.ts new file mode 100644 index 0000000..6c12fe1 --- /dev/null +++ b/backend/src/migrations/1775500002000-AddProjectHoursOverrides.ts @@ -0,0 +1,15 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class AddProjectHoursOverrides1775500002000 implements MigrationInterface { + name = 'AddProjectHoursOverrides1775500002000' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "projects" ADD "override_hours" real`); + await queryRunner.query(`ALTER TABLE "projects" ADD "internal_hours" real`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "projects" DROP COLUMN "internal_hours"`); + await queryRunner.query(`ALTER TABLE "projects" DROP COLUMN "override_hours"`); + } +} diff --git a/backend/src/migrations/1775600000000-CreateComments.ts b/backend/src/migrations/1775600000000-CreateComments.ts new file mode 100644 index 0000000..9364d19 --- /dev/null +++ b/backend/src/migrations/1775600000000-CreateComments.ts @@ -0,0 +1,25 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class CreateComments1775600000000 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + CREATE TABLE "comments" ( + "id" uuid NOT NULL DEFAULT gen_random_uuid(), + "project_id" uuid NOT NULL, + "user_id" uuid NOT NULL, + "body" varchar(500) NOT NULL, + "created_at" timestamp NOT NULL DEFAULT now(), + CONSTRAINT "PK_comments" PRIMARY KEY ("id"), + CONSTRAINT "FK_comments_project" FOREIGN KEY ("project_id") REFERENCES "projects"("id") ON DELETE CASCADE, + CONSTRAINT "FK_comments_user" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE + ) + `); + + await queryRunner.query(`CREATE INDEX "IDX_comments_project_id" ON "comments" ("project_id")`); + await queryRunner.query(`CREATE INDEX "IDX_comments_user_id" ON "comments" ("user_id")`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP TABLE "comments"`); + } +} diff --git a/backend/src/migrations/1775800000000-CreateShopItems.ts b/backend/src/migrations/1775800000000-CreateShopItems.ts new file mode 100644 index 0000000..29be19b --- /dev/null +++ b/backend/src/migrations/1775800000000-CreateShopItems.ts @@ -0,0 +1,29 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class CreateShopItems1775800000000 implements MigrationInterface { + name = 'CreateShopItems1775800000000' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + CREATE TABLE "shop_items" ( + "id" uuid NOT NULL DEFAULT uuid_generate_v4(), + "name" varchar(200) NOT NULL, + "description" varchar(500) NOT NULL, + "image_url" varchar(500) NOT NULL, + "price_hours" integer NOT NULL, + "stock" integer, + "sort_order" integer NOT NULL DEFAULT 0, + "is_active" boolean NOT NULL DEFAULT true, + "estimated_ship" varchar(200), + "created_at" TIMESTAMP NOT NULL DEFAULT now(), + "updated_at" TIMESTAMP NOT NULL DEFAULT now(), + CONSTRAINT "PK_shop_items" PRIMARY KEY ("id") + ) + `); + await queryRunner.query(`CREATE INDEX "IDX_shop_items_sort_order" ON "shop_items"("sort_order")`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP TABLE "shop_items"`); + } +} diff --git a/backend/src/migrations/1775900000000-AddShippingEligibility.ts b/backend/src/migrations/1775900000000-AddShippingEligibility.ts new file mode 100644 index 0000000..7f937a4 --- /dev/null +++ b/backend/src/migrations/1775900000000-AddShippingEligibility.ts @@ -0,0 +1,16 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class AddShippingEligibility1775900000000 implements MigrationInterface { + name = 'AddShippingEligibility1775900000000' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "users" ADD "has_address" boolean NOT NULL DEFAULT false`); + await queryRunner.query(`ALTER TABLE "users" ADD "has_birthdate" boolean NOT NULL DEFAULT false`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "users" DROP COLUMN "has_birthdate"`); + await queryRunner.query(`ALTER TABLE "users" DROP COLUMN "has_address"`); + } + +} diff --git a/backend/src/migrations/1776000000000-CreateOrdersAndFulfillment.ts b/backend/src/migrations/1776000000000-CreateOrdersAndFulfillment.ts new file mode 100644 index 0000000..835aca1 --- /dev/null +++ b/backend/src/migrations/1776000000000-CreateOrdersAndFulfillment.ts @@ -0,0 +1,58 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class CreateOrdersAndFulfillment1776000000000 implements MigrationInterface { + name = 'CreateOrdersAndFulfillment1776000000000' + + public async up(queryRunner: QueryRunner): Promise { + // Add pipes column to users + await queryRunner.query(`ALTER TABLE "users" ADD "pipes" integer NOT NULL DEFAULT 0`); + + // Add pipes_granted to projects (tracks how many pipes were already granted to prevent double-granting) + await queryRunner.query(`ALTER TABLE "projects" ADD "pipes_granted" integer NOT NULL DEFAULT 0`); + + // Create orders table + await queryRunner.query(` + CREATE TABLE "orders" ( + "id" uuid NOT NULL DEFAULT uuid_generate_v4(), + "user_id" uuid NOT NULL, + "shop_item_id" uuid, + "quantity" integer NOT NULL, + "pipes_spent" integer NOT NULL, + "item_name" varchar(200) NOT NULL, + "status" varchar(20) NOT NULL DEFAULT 'pending', + "created_at" TIMESTAMP NOT NULL DEFAULT now(), + "updated_at" TIMESTAMP NOT NULL DEFAULT now(), + CONSTRAINT "PK_orders" PRIMARY KEY ("id"), + CONSTRAINT "FK_orders_user" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE, + CONSTRAINT "FK_orders_shop_item" FOREIGN KEY ("shop_item_id") REFERENCES "shop_items"("id") ON DELETE SET NULL + ) + `); + await queryRunner.query(`CREATE INDEX "IDX_orders_user_id" ON "orders"("user_id")`); + await queryRunner.query(`CREATE INDEX "IDX_orders_status" ON "orders"("status")`); + await queryRunner.query(`CREATE INDEX "IDX_orders_created_at" ON "orders"("created_at")`); + + // Create fulfillment_updates table + await queryRunner.query(` + CREATE TABLE "fulfillment_updates" ( + "id" uuid NOT NULL DEFAULT uuid_generate_v4(), + "user_id" uuid NOT NULL, + "order_id" uuid NOT NULL, + "message" varchar(500) NOT NULL, + "is_read" boolean NOT NULL DEFAULT false, + "created_at" TIMESTAMP NOT NULL DEFAULT now(), + CONSTRAINT "PK_fulfillment_updates" PRIMARY KEY ("id"), + CONSTRAINT "FK_fulfillment_updates_user" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE, + CONSTRAINT "FK_fulfillment_updates_order" FOREIGN KEY ("order_id") REFERENCES "orders"("id") ON DELETE CASCADE + ) + `); + await queryRunner.query(`CREATE INDEX "IDX_fulfillment_updates_user_id" ON "fulfillment_updates"("user_id")`); + await queryRunner.query(`CREATE INDEX "IDX_fulfillment_updates_user_read" ON "fulfillment_updates"("user_id", "is_read")`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP TABLE "fulfillment_updates"`); + await queryRunner.query(`DROP TABLE "orders"`); + await queryRunner.query(`ALTER TABLE "projects" DROP COLUMN "pipes_granted"`); + await queryRunner.query(`ALTER TABLE "users" DROP COLUMN "pipes"`); + } +} diff --git a/backend/src/migrations/1776100000000-CreateSubmissions.ts b/backend/src/migrations/1776100000000-CreateSubmissions.ts new file mode 100644 index 0000000..1c38358 --- /dev/null +++ b/backend/src/migrations/1776100000000-CreateSubmissions.ts @@ -0,0 +1,37 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class CreateSubmissions1776100000000 implements MigrationInterface { + name = 'CreateSubmissions1776100000000' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + CREATE TABLE "submissions" ( + "id" uuid NOT NULL DEFAULT uuid_generate_v4(), + "project_id" uuid NOT NULL, + "user_id" uuid NOT NULL, + "change_description" text, + "min_hours_confirmed" boolean NOT NULL DEFAULT false, + "status" varchar(20) NOT NULL DEFAULT 'unreviewed', + "override_hours" real, + "internal_hours" real, + "pipes_granted" integer NOT NULL DEFAULT 0, + "created_at" TIMESTAMP NOT NULL DEFAULT now(), + CONSTRAINT "PK_submissions" PRIMARY KEY ("id"), + CONSTRAINT "FK_submissions_project" FOREIGN KEY ("project_id") REFERENCES "projects"("id") ON DELETE CASCADE, + CONSTRAINT "FK_submissions_user" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE + ) + `); + await queryRunner.query(`CREATE INDEX "IDX_submissions_project_id" ON "submissions"("project_id")`); + await queryRunner.query(`CREATE INDEX "IDX_submissions_status" ON "submissions"("status")`); + + // Link project_reviews to submissions instead of directly to projects + await queryRunner.query(`ALTER TABLE "project_reviews" ADD "submission_id" uuid`); + await queryRunner.query(`ALTER TABLE "project_reviews" ADD CONSTRAINT "FK_project_reviews_submission" FOREIGN KEY ("submission_id") REFERENCES "submissions"("id") ON DELETE CASCADE`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "project_reviews" DROP CONSTRAINT "FK_project_reviews_submission"`); + await queryRunner.query(`ALTER TABLE "project_reviews" DROP COLUMN "submission_id"`); + await queryRunner.query(`DROP TABLE "submissions"`); + } +} diff --git a/backend/src/news/news.controller.ts b/backend/src/news/news.controller.ts new file mode 100644 index 0000000..7e0a319 --- /dev/null +++ b/backend/src/news/news.controller.ts @@ -0,0 +1,22 @@ +import { Controller, Get, UseGuards } from '@nestjs/common'; +import { JwtAuthGuard } from '../auth/jwt-auth.guard'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { NewsItem } from '../entities/news-item.entity'; + +@Controller('api/news') +export class NewsController { + constructor( + @InjectRepository(NewsItem) + private readonly newsRepo: Repository, + ) {} + + @UseGuards(JwtAuthGuard) + @Get() + async list() { + return this.newsRepo.find({ + order: { displayDate: 'DESC', createdAt: 'DESC' }, + select: ['id', 'text', 'displayDate'], + }); + } +} diff --git a/backend/src/news/news.module.ts b/backend/src/news/news.module.ts new file mode 100644 index 0000000..82e7bf2 --- /dev/null +++ b/backend/src/news/news.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { AuthModule } from '../auth/auth.module'; +import { NewsItem } from '../entities/news-item.entity'; +import { NewsController } from './news.controller'; + +@Module({ + imports: [TypeOrmModule.forFeature([NewsItem]), AuthModule], + controllers: [NewsController], +}) +export class NewsModule {} diff --git a/backend/src/onboarding/onboarding.controller.ts b/backend/src/onboarding/onboarding.controller.ts new file mode 100644 index 0000000..73d6ae9 --- /dev/null +++ b/backend/src/onboarding/onboarding.controller.ts @@ -0,0 +1,100 @@ +import { Controller, Get, Post, Req, UseGuards, Logger } from '@nestjs/common'; +import { Throttle } from '@nestjs/throttler'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import type { Request } from 'express'; +import { JwtAuthGuard } from '../auth/jwt-auth.guard'; +import { HackatimeService } from '../hackatime/hackatime.service'; +import { SlackService } from '../slack/slack.service'; +import { ProjectsService } from '../projects/projects.service'; +import { RsvpService } from '../rsvp/rsvp.service'; +import { User } from '../entities/user.entity'; + +@Controller('api/onboarding') +export class OnboardingController { + private readonly logger = new Logger(OnboardingController.name); + + constructor( + private readonly hackatimeService: HackatimeService, + private readonly slackService: SlackService, + private readonly projectsService: ProjectsService, + private readonly rsvpService: RsvpService, + @InjectRepository(User) + private readonly userRepo: Repository, + ) {} + + @Throttle({ default: { limit: 5, ttl: 60000 } }) + @UseGuards(JwtAuthGuard) + @Post('two-emails') + async setTwoEmails(@Req() req: Request) { + const user = (req as any).user; + await this.userRepo.update({ hcaSub: user.sub }, { twoEmails: true }); + return { ok: true }; + } + + /** + * Returns completion status for each onboarding step. + * The frontend uses this to show "Complete! Move on?" vs action buttons. + */ + @Throttle({ default: { limit: 15, ttl: 60000 } }) + @UseGuards(JwtAuthGuard) + @Get('status') + async getStatus(@Req() req: Request) { + const user = (req as any).user; + + // Check Slack membership by email + let slack: 'full_member' | 'guest' | 'not_found' | 'error' = 'not_found'; + try { + const dbUser = await this.userRepo.findOne({ + where: { hcaSub: user.sub }, + select: ['email', 'twoEmails'], + }); + if (dbUser?.twoEmails) { + // User confirmed they use a different email on Slack — skip email lookup + slack = 'full_member'; + } else if (dbUser?.email) { + slack = await this.slackService.checkMembership(dbUser.email); + } + } catch (err) { + this.logger.error(`Slack membership check failed for ${user.sub}: ${err}`); + slack = 'error'; + } + + const [hackatime, project] = await Promise.all([ + this.hackatimeService.isConnected(user.sub), + this.projectsService.userHasProjects(user.uid), + ]); + + // Sync tutorial completion date to Airtable for Loops + const slackDone = slack === 'full_member'; + if (hackatime && slackDone && project) { + const dbUserForSync = await this.userRepo.findOne({ + where: { hcaSub: user.sub }, + select: ['email'], + }); + if (dbUserForSync?.email) { + this.rsvpService.updateDateField(dbUserForSync.email, 'Loops - beestCompletedTutorial'); + } + } + + return { hackatime, slack, project }; + } + + @Throttle({ default: { limit: 10, ttl: 60000 } }) + @UseGuards(JwtAuthGuard) + @Get('sticker-link') + async getStickerLink(@Req() req: Request) { + const user = (req as any).user; + const dbUser = await this.userRepo.findOne({ + where: { hcaSub: user.sub }, + select: ['email'], + }); + + if (!dbUser?.email) { + return { link: null }; + } + + const link = await this.rsvpService.getStickerLink(dbUser.email); + return { link }; + } +} diff --git a/backend/src/onboarding/onboarding.module.ts b/backend/src/onboarding/onboarding.module.ts new file mode 100644 index 0000000..42c19df --- /dev/null +++ b/backend/src/onboarding/onboarding.module.ts @@ -0,0 +1,15 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { AuthModule } from '../auth/auth.module'; +import { HackatimeModule } from '../hackatime/hackatime.module'; +import { SlackModule } from '../slack/slack.module'; +import { ProjectsModule } from '../projects/projects.module'; +import { RsvpModule } from '../rsvp/rsvp.module'; +import { User } from '../entities/user.entity'; +import { OnboardingController } from './onboarding.controller'; + +@Module({ + imports: [AuthModule, HackatimeModule, SlackModule, ProjectsModule, RsvpModule, TypeOrmModule.forFeature([User])], + controllers: [OnboardingController], +}) +export class OnboardingModule {} diff --git a/backend/src/projects/create-project.dto.ts b/backend/src/projects/create-project.dto.ts new file mode 100644 index 0000000..6bf94f0 --- /dev/null +++ b/backend/src/projects/create-project.dto.ts @@ -0,0 +1,13 @@ +export class CreateProjectDto { + name: string; + description: string; + projectType: string; + codeUrl?: string; + readmeUrl?: string; + demoUrl?: string; + screenshots?: string[]; + hackatimeProjectName?: string[]; + isUpdate?: boolean; + otherHcProgram?: string; + aiUse?: string; +} diff --git a/backend/src/projects/leaderboard.controller.ts b/backend/src/projects/leaderboard.controller.ts new file mode 100644 index 0000000..d892309 --- /dev/null +++ b/backend/src/projects/leaderboard.controller.ts @@ -0,0 +1,56 @@ +import { Controller, Get, UseGuards } from '@nestjs/common'; +import { Throttle } from '@nestjs/throttler'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { JwtAuthGuard } from '../auth/jwt-auth.guard'; +import { HackatimeService } from '../hackatime/hackatime.service'; +import { User } from '../entities/user.entity'; +import { ProjectsService } from './projects.service'; + +@Controller('api/leaderboard') +export class LeaderboardController { + constructor( + private readonly projectsService: ProjectsService, + private readonly hackatimeService: HackatimeService, + @InjectRepository(User) + private readonly userRepo: Repository, + ) {} + + /** + * Returns the top 10 users by approved Hackatime hours. + * No PII is exposed — only display names and hours. + */ + @Throttle({ default: { limit: 5, ttl: 60000 } }) + @UseGuards(JwtAuthGuard) + @Get() + async getLeaderboard() { + const [grouped, totalUsers] = await Promise.all([ + this.projectsService.findApprovedProjectsGroupedByUser(), + this.userRepo.count(), + ]); + + const results = await Promise.allSettled( + Array.from(grouped.entries()).map(async ([, entry]) => { + const { hours } = await this.hackatimeService.getHoursForProjects( + entry.hcaSub, + entry.projectNames, + ); + return { + name: entry.nickname || entry.name || 'Anonymous', + hours, + }; + }), + ); + + const leaderboard = results + .filter( + (r): r is PromiseFulfilledResult<{ name: string; hours: number }> => + r.status === 'fulfilled' && r.value.hours > 0, + ) + .map((r) => r.value) + .sort((a, b) => b.hours - a.hours) + .slice(0, 10); + + return { leaderboard, totalUsers }; + } +} diff --git a/backend/src/projects/projects.controller.ts b/backend/src/projects/projects.controller.ts new file mode 100644 index 0000000..b3ba3e1 --- /dev/null +++ b/backend/src/projects/projects.controller.ts @@ -0,0 +1,214 @@ +import { + Controller, + Post, + Get, + Patch, + Delete, + Body, + Param, + Req, + UseGuards, + UnauthorizedException, +} from '@nestjs/common'; +import { Throttle } from '@nestjs/throttler'; +import type { Request } from 'express'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { JwtAuthGuard } from '../auth/jwt-auth.guard'; +import { HackatimeService } from '../hackatime/hackatime.service'; +import { Project } from '../entities/project.entity'; +import { ProjectReview } from '../entities/project-review.entity'; +import { ProjectsService } from './projects.service'; +import { CreateProjectDto } from './create-project.dto'; +import { UpdateProjectDto } from './update-project.dto'; + +@Controller('api/projects') +export class ProjectsController { + constructor( + private readonly projectsService: ProjectsService, + private readonly hackatimeService: HackatimeService, + @InjectRepository(Project) private readonly projectRepo: Repository, + @InjectRepository(ProjectReview) private readonly reviewRepo: Repository, + ) {} + + /** + * Returns total Hackatime hours across all of the user's linked projects. + * Called on page load — Hackatime time increases without site interaction. + */ + @Throttle({ default: { limit: 15, ttl: 60000 } }) + @UseGuards(JwtAuthGuard) + @Get('hours') + async getHours(@Req() req: Request) { + const user = (req as any).user; + if (!user?.uid) throw new UnauthorizedException('No user identity'); + + const projects = await this.projectsService.findByUser(user.uid); + const linkedNames = projects + .flatMap((p) => p.hackatimeProjectName ?? []) + .filter((n): n is string => !!n); + + const { hours, perProject } = await this.hackatimeService.getHoursForProjects( + user.sub, + [...new Set(linkedNames)], + ); + + const byStatus: Record = {}; + for (const p of projects) { + const names = p.hackatimeProjectName ?? []; + const status = p.status ?? 'unshipped'; + for (const name of names) { + if (perProject[name]) { + byStatus[status] = (byStatus[status] ?? 0) + perProject[name]; + } + } + } + + return { hours, byStatus }; + } + + @Throttle({ default: { limit: 10, ttl: 60000 } }) + @UseGuards(JwtAuthGuard) + @Get('explore') + async explore() { + return this.projectsService.findApprovedProjects(); + } + + @Throttle({ default: { limit: 15, ttl: 60000 } }) + @UseGuards(JwtAuthGuard) + @Get('explore/:id') + async exploreDetail(@Param('id') id: string) { + const project = await this.projectsService.findApprovedProjectById(id); + if (!project) throw new UnauthorizedException('Project not found'); + return project; + } + + @Throttle({ default: { limit: 30, ttl: 60000 } }) + @UseGuards(JwtAuthGuard) + @Get('explore/:id/comments') + async getComments(@Param('id') projectId: string) { + return this.projectsService.getComments(projectId); + } + + @Throttle({ default: { limit: 10, ttl: 60000 } }) + @UseGuards(JwtAuthGuard) + @Post('explore/:id/comments') + async addComment( + @Param('id') projectId: string, + @Req() req: Request, + @Body() body: { body?: string }, + ) { + const user = (req as any).user; + if (!user?.uid) throw new UnauthorizedException('No user identity'); + return this.projectsService.addComment(projectId, user.uid, body.body ?? ''); + } + + @Throttle({ default: { limit: 10, ttl: 60000 } }) + @UseGuards(JwtAuthGuard) + @Delete('explore/:id/comments/:commentId') + async deleteComment( + @Param('commentId') commentId: string, + @Req() req: Request, + ) { + const user = (req as any).user; + if (!user?.uid) throw new UnauthorizedException('No user identity'); + return this.projectsService.deleteComment(commentId, user.uid, user.email); + } + + @Throttle({ default: { limit: 10, ttl: 60000 } }) + @UseGuards(JwtAuthGuard) + @Post() + async create(@Req() req: Request, @Body() dto: CreateProjectDto) { + const user = (req as any).user; + if (!user?.uid) throw new UnauthorizedException('No user identity'); + + return this.projectsService.create(dto, user.uid, user.sub, user.impersonator_name); + } + + @Throttle({ default: { limit: 30, ttl: 60000 } }) + @UseGuards(JwtAuthGuard) + @Get() + async list(@Req() req: Request) { + const userId = (req as any).user?.uid; + if (!userId) throw new UnauthorizedException('No user identity'); + + return this.projectsService.findByUser(userId); + } + + @Throttle({ default: { limit: 10, ttl: 60000 } }) + @UseGuards(JwtAuthGuard) + @Patch(':id') + async update( + @Param('id') id: string, + @Req() req: Request, + @Body() dto: UpdateProjectDto, + ) { + const user = (req as any).user; + if (!user?.uid) throw new UnauthorizedException('No user identity'); + + return this.projectsService.update(id, dto, user.uid, user.sub, user.impersonator_name); + } + + @Throttle({ default: { limit: 10, ttl: 60000 } }) + @UseGuards(JwtAuthGuard) + @Post(':id/resubmit') + async resubmit( + @Param('id') id: string, + @Req() req: Request, + @Body() body: { changeDescription?: string; minHoursConfirmed?: boolean }, + ) { + const user = (req as any).user; + if (!user?.uid) throw new UnauthorizedException('No user identity'); + if (!body.changeDescription || typeof body.changeDescription !== 'string') { + throw new UnauthorizedException('changeDescription is required'); + } + return this.projectsService.resubmit( + id, + user.uid, + user.sub, + body.changeDescription, + body.minHoursConfirmed === true, + user.impersonator_name, + ); + } + + @Throttle({ default: { limit: 10, ttl: 60000 } }) + @UseGuards(JwtAuthGuard) + @Delete(':id') + async delete(@Param('id') id: string, @Req() req: Request) { + const user = (req as any).user; + if (!user?.uid) throw new UnauthorizedException('No user identity'); + + await this.projectsService.delete(id, user.uid, user.impersonator_name); + return { deleted: true }; + } + + @Throttle({ default: { limit: 30, ttl: 60000 } }) + @UseGuards(JwtAuthGuard) + @Get(':id/reviews') + async getReviews(@Param('id') id: string, @Req() req: Request) { + const userId = (req as any).user?.uid; + if (!userId) throw new UnauthorizedException('No user identity'); + + // Verify the project belongs to the authenticated user + const project = await this.projectRepo.findOne({ + where: { id, userId }, + select: ['id'], + }); + if (!project) throw new UnauthorizedException('Project not found'); + + const reviews = await this.reviewRepo.find({ + where: { projectId: id }, + order: { createdAt: 'DESC' }, + relations: ['reviewer'], + }); + + // Never expose internal notes to the user + return reviews.map((r) => ({ + id: r.id, + status: r.status, + feedback: r.feedback, + reviewerName: r.reviewer?.name ?? null, + createdAt: r.createdAt, + })); + } +} diff --git a/backend/src/projects/projects.module.ts b/backend/src/projects/projects.module.ts new file mode 100644 index 0000000..09c7ecf --- /dev/null +++ b/backend/src/projects/projects.module.ts @@ -0,0 +1,22 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { AuthModule } from '../auth/auth.module'; +import { AuditLogModule } from '../audit-log/audit-log.module'; +import { HackatimeModule } from '../hackatime/hackatime.module'; +import { RsvpModule } from '../rsvp/rsvp.module'; +import { Project } from '../entities/project.entity'; +import { ProjectReview } from '../entities/project-review.entity'; +import { Comment } from '../entities/comment.entity'; +import { Submission } from '../entities/submission.entity'; +import { User } from '../entities/user.entity'; +import { ProjectsController } from './projects.controller'; +import { LeaderboardController } from './leaderboard.controller'; +import { ProjectsService } from './projects.service'; + +@Module({ + imports: [AuthModule, AuditLogModule, HackatimeModule, RsvpModule, TypeOrmModule.forFeature([Project, ProjectReview, Comment, Submission, User])], + controllers: [ProjectsController, LeaderboardController], + providers: [ProjectsService], + exports: [ProjectsService], +}) +export class ProjectsModule {} diff --git a/backend/src/projects/projects.service.ts b/backend/src/projects/projects.service.ts new file mode 100644 index 0000000..ccdcb59 --- /dev/null +++ b/backend/src/projects/projects.service.ts @@ -0,0 +1,850 @@ +import { Injectable, BadRequestException, NotFoundException, ForbiddenException, Logger } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { Project, VALID_PROJECT_TYPES } from '../entities/project.entity'; +import { Comment } from '../entities/comment.entity'; +import { Submission } from '../entities/submission.entity'; +import { User } from '../entities/user.entity'; +import { fetchWithTimeout } from '../fetch.util'; +import { AuditLogService } from '../audit-log/audit-log.service'; +import { HackatimeService } from '../hackatime/hackatime.service'; +import { RsvpService } from '../rsvp/rsvp.service'; +import { CreateProjectDto } from './create-project.dto'; +import { UpdateProjectDto } from './update-project.dto'; + +const CDN_UPLOAD_URL = 'https://cdn.hackclub.com/api/v4/upload'; + +/** MIME → file extension mapping for uploaded screenshots. */ +const MIME_EXTENSIONS: Record = { + 'image/png': 'png', + 'image/jpeg': 'jpg', + 'image/gif': 'gif', + 'image/webp': 'webp', +}; + +/** PNG, JPEG, GIF, WEBP magic-byte prefixes (base64-encoded first bytes). */ +const IMAGE_SIGNATURES: { mime: string; b64Prefix: string }[] = [ + { mime: 'image/png', b64Prefix: 'iVBOR' }, + { mime: 'image/jpeg', b64Prefix: '/9j/' }, + { mime: 'image/gif', b64Prefix: 'R0lGOD' }, + { mime: 'image/webp', b64Prefix: 'UklGR' }, +]; + +@Injectable() +export class ProjectsService { + private readonly logger = new Logger(ProjectsService.name); + private readonly cdnApiKey: string; + + constructor( + private configService: ConfigService, + private auditLogService: AuditLogService, + private hackatimeService: HackatimeService, + private rsvpService: RsvpService, + @InjectRepository(Project) + private projectRepo: Repository, + @InjectRepository(Comment) + private commentRepo: Repository, + @InjectRepository(Submission) + private submissionRepo: Repository, + @InjectRepository(User) + private userRepo: Repository, + ) { + this.cdnApiKey = this.configService.getOrThrow('CDN_API_KEY'); + } + + /* ------------------------------------------------------------------ */ + /* Public */ + /* ------------------------------------------------------------------ */ + + async create( + dto: CreateProjectDto, + userId: string, + hcaSub: string, + impersonatorName?: string, + ) { + // --- required fields --- + const name = this.requireString(dto.name, 'name', 50); + const description = this.requireString(dto.description, 'description', 300); + const projectType = this.validateProjectType(dto.projectType); + + // --- optional URLs --- + const codeUrl = this.validateUrl(dto.codeUrl, 'codeUrl'); + const readmeUrl = this.validateUrl(dto.readmeUrl, 'readmeUrl'); + const demoUrl = this.validateUrl(dto.demoUrl, 'demoUrl'); + + // --- optional screenshots (max 2) — validate then upload to CDN --- + const validated = this.validateScreenshots(dto.screenshots); + const screenshotUrls = await this.uploadScreenshots(validated); + + // --- optional hackatime project names (validated against real projects) --- + const hackatimeProjectName: string[] = []; + if (dto.hackatimeProjectName && Array.isArray(dto.hackatimeProjectName) && dto.hackatimeProjectName.length > 0) { + const realProjects = await this.hackatimeService.getProjectNames(hcaSub); + for (const raw of dto.hackatimeProjectName) { + if (typeof raw !== 'string') continue; + const cleaned = this.sanitize(raw).slice(0, 255); + if (!cleaned) continue; + if (!realProjects.includes(cleaned)) { + throw new BadRequestException( + `Hackatime project "${cleaned}" was not found on your account`, + ); + } + hackatimeProjectName.push(cleaned); + } + } + + const isUpdate = dto.isUpdate === true; + const otherHcProgram = this.validateOptionalString(dto.otherHcProgram, 'otherHcProgram', 255); + const aiUse = this.validateOptionalString(dto.aiUse, 'aiUse', 200); + + const project = this.projectRepo.create({ + userId, + name, + description, + projectType, + codeUrl, + readmeUrl, + demoUrl, + screenshot1Url: screenshotUrls[0], + screenshot2Url: screenshotUrls[1], + hackatimeProjectName, + isUpdate, + otherHcProgram, + aiUse, + }); + + const saved = await this.projectRepo.save(project); + + await this.auditLogService.log( + userId, + 'project_created', + `Created project "${name}"`, + impersonatorName, + ); + + // Strip internal fields before returning to frontend + const { userId: _uid, user: _user, ...safe } = saved; + return safe; + } + + async findByUser(userId: string) { + const projects = await this.projectRepo.find({ + where: { userId }, + order: { createdAt: 'DESC' }, + select: [ + 'id', + 'name', + 'description', + 'projectType', + 'codeUrl', + 'readmeUrl', + 'demoUrl', + 'screenshot1Url', + 'screenshot2Url', + 'hackatimeProjectName', + 'status', + 'isUpdate', + 'otherHcProgram', + 'aiUse', + 'createdAt', + 'updatedAt', + ], + }); + return projects; + } + + /** + * Returns all approved projects with public-safe fields + hours. + */ + async findApprovedProjects(): Promise< + { + id: string; + name: string; + description: string; + projectType: string; + screenshot1Url: string | null; + screenshot2Url: string | null; + codeUrl: string | null; + demoUrl: string | null; + hours: number; + builderName: string; + }[] + > { + const projects = await this.projectRepo + .createQueryBuilder('project') + .innerJoinAndSelect('project.user', 'user') + .where('project.status = :status', { status: 'approved' }) + .select([ + 'project.id', + 'project.name', + 'project.description', + 'project.projectType', + 'project.screenshot1Url', + 'project.screenshot2Url', + 'project.codeUrl', + 'project.demoUrl', + 'project.hackatimeProjectName', + 'project.overrideHours', + 'user.hcaSub', + 'user.name', + 'user.nickname', + 'user.hackatimeToken', + ]) + .getMany(); + + const results = await Promise.allSettled( + projects.map(async (p) => { + const names = (p.hackatimeProjectName ?? []).filter((n) => !!n); + let hours = 0; + if (p.overrideHours != null) { + hours = p.overrideHours; + } else if (names.length > 0 && p.user.hackatimeToken) { + const result = await this.hackatimeService.getHoursForProjects( + p.user.hcaSub, + names, + ); + hours = result.hours; + } + return { + id: p.id, + name: p.name, + description: p.description, + projectType: p.projectType, + screenshot1Url: p.screenshot1Url, + screenshot2Url: p.screenshot2Url, + codeUrl: p.codeUrl, + demoUrl: p.demoUrl, + hours, + builderName: p.user.nickname || p.user.name || 'Anonymous', + }; + }), + ); + + return results + .filter( + (r): r is PromiseFulfilledResult => r.status === 'fulfilled', + ) + .map((r) => r.value); + } + + async userHasProjects(userId: string): Promise { + const count = await this.projectRepo.count({ where: { userId } }); + return count > 0; + } + + /** + * Returns approved projects grouped by user, including user name info. + * Only includes users who have a hackatime token (needed to fetch hours). + */ + async findApprovedProjectsGroupedByUser(): Promise< + Map + > { + const projects = await this.projectRepo + .createQueryBuilder('project') + .innerJoinAndSelect('project.user', 'user') + .where('project.status = :status', { status: 'approved' }) + .andWhere('user.hackatime_token IS NOT NULL') + .andWhere('project.hackatime_project_name IS NOT NULL') + .select([ + 'project.id', + 'project.hackatimeProjectName', + 'user.id', + 'user.hcaSub', + 'user.name', + 'user.nickname', + ]) + .getMany(); + + const grouped = new Map< + string, + { hcaSub: string; name: string | null; nickname: string | null; projectNames: string[] } + >(); + + for (const p of projects) { + const userId = p.user.id; + const names = (p.hackatimeProjectName ?? []).filter((n) => !!n); + if (names.length === 0) continue; + + if (!grouped.has(userId)) { + grouped.set(userId, { + hcaSub: p.user.hcaSub, + name: p.user.name, + nickname: p.user.nickname, + projectNames: [], + }); + } + grouped.get(userId)!.projectNames.push(...names); + } + + // Deduplicate project names per user + for (const entry of grouped.values()) { + entry.projectNames = [...new Set(entry.projectNames)]; + } + + return grouped; + } + + async update( + projectId: string, + dto: UpdateProjectDto, + userId: string, + hcaSub: string, + impersonatorName?: string, + ) { + const project = await this.projectRepo.findOne({ + where: { id: projectId, userId }, + }); + if (!project) throw new NotFoundException('Project not found'); + + if (dto.name !== undefined) { + project.name = this.requireString(dto.name, 'name', 50); + } + if (dto.description !== undefined) { + project.description = this.requireString(dto.description, 'description', 300); + } + if (dto.projectType !== undefined) { + project.projectType = this.validateProjectType(dto.projectType); + } + if (dto.codeUrl !== undefined) { + project.codeUrl = dto.codeUrl === null ? null : this.validateUrl(dto.codeUrl, 'codeUrl'); + } + if (dto.readmeUrl !== undefined) { + project.readmeUrl = dto.readmeUrl === null ? null : this.validateUrl(dto.readmeUrl, 'readmeUrl'); + } + if (dto.demoUrl !== undefined) { + project.demoUrl = dto.demoUrl === null ? null : this.validateUrl(dto.demoUrl, 'demoUrl'); + } + if (dto.screenshots !== undefined) { + const validated = this.validateScreenshots(dto.screenshots); + const screenshotUrls = await this.uploadScreenshots(validated); + project.screenshot1Url = screenshotUrls[0] ?? null; + project.screenshot2Url = screenshotUrls[1] ?? null; + } + if (dto.hackatimeProjectName !== undefined) { + if (dto.hackatimeProjectName === null || (Array.isArray(dto.hackatimeProjectName) && dto.hackatimeProjectName.length === 0)) { + project.hackatimeProjectName = []; + } else if (Array.isArray(dto.hackatimeProjectName)) { + const realProjects = await this.hackatimeService.getProjectNames(hcaSub); + const validated: string[] = []; + for (const raw of dto.hackatimeProjectName) { + if (typeof raw !== 'string') continue; + const cleaned = this.sanitize(raw).slice(0, 255); + if (!cleaned) continue; + if (!realProjects.includes(cleaned)) { + throw new BadRequestException( + `Hackatime project "${cleaned}" was not found on your account`, + ); + } + validated.push(cleaned); + } + project.hackatimeProjectName = validated; + } + } + if (dto.isUpdate !== undefined) { + project.isUpdate = dto.isUpdate === true; + } + if (dto.otherHcProgram !== undefined) { + project.otherHcProgram = dto.otherHcProgram === null ? null : this.validateOptionalString(dto.otherHcProgram, 'otherHcProgram', 255); + } + if (dto.aiUse !== undefined) { + project.aiUse = dto.aiUse === null ? null : this.validateOptionalString(dto.aiUse, 'aiUse', 200); + } + if (dto.status !== undefined) { + if (dto.status === 'unreviewed') { + if (project.status !== 'unshipped' && project.status !== 'changes_needed') { + throw new BadRequestException('Invalid status transition'); + } + project.status = 'unreviewed'; + } else if ( + dto.status === 'unshipped' && + project.status === 'unreviewed' + ) { + project.status = 'unshipped'; + } else { + throw new BadRequestException( + 'Invalid status transition', + ); + } + } + + const saved = await this.projectRepo.save(project); + + if (dto.status === 'unreviewed') { + // Create a submission record for this review request + const submission = this.submissionRepo.create({ + projectId: project.id, + userId, + changeDescription: null, + minHoursConfirmed: false, + status: 'unreviewed', + }); + await this.submissionRepo.save(submission); + + await this.auditLogService.log( + userId, + 'project_submitted', + `Submitted "${project.name}" for review`, + impersonatorName, + ); + + // Sync submission date to Airtable for Loops + this.userRepo.findOne({ where: { id: userId }, select: ['email'] }).then((u) => { + if (u?.email) this.rsvpService.updateDateField(u.email, 'Loops - beestShippedProject'); + }); + } else if (dto.status === 'unshipped') { + await this.auditLogService.log( + userId, + 'project_updated', + `Converted "${project.name}" back to draft`, + impersonatorName, + ); + } else { + await this.auditLogService.log( + userId, + 'project_updated', + `Updated project "${project.name}"`, + impersonatorName, + ); + } + + const { userId: _uid, user: _user, ...safe } = saved; + return safe; + } + + async delete(projectId: string, userId: string, impersonatorName?: string) { + const project = await this.projectRepo.findOne({ + where: { id: projectId, userId }, + }); + if (!project) throw new NotFoundException('Project not found'); + if (project.status === 'approved') { + throw new ForbiddenException('Approved projects cannot be deleted'); + } + + const name = project.name; + await this.projectRepo.remove(project); + + await this.auditLogService.log( + userId, + 'project_deleted', + `Deleted project "${name}"`, + impersonatorName, + ); + } + + /* ------------------------------------------------------------------ */ + /* Resubmit (approved → unreviewed with change description) */ + /* ------------------------------------------------------------------ */ + + async resubmit( + projectId: string, + userId: string, + hcaSub: string, + changeDescription: string, + minHoursConfirmed: boolean, + impersonatorName?: string, + ) { + const project = await this.projectRepo.findOne({ + where: { id: projectId, userId }, + }); + if (!project) throw new NotFoundException('Project not found'); + if (project.status !== 'approved') { + throw new BadRequestException('Only approved projects can be resubmitted'); + } + + // Validate inputs + const cleanDesc = this.requireString(changeDescription, 'changeDescription', 500); + if (!minHoursConfirmed) { + throw new BadRequestException('You must confirm at least 3 hours of work since the last ship'); + } + + // Verify at least 3 hours of new Hackatime work since last approval + const linkedNames = (project.hackatimeProjectName ?? []).filter((n) => !!n); + const previousApprovedHours = project.overrideHours ?? 0; + if (linkedNames.length > 0) { + try { + const { hours: currentHours } = await this.hackatimeService.getHoursForProjects( + hcaSub, + [...new Set(linkedNames)], + ); + const delta = currentHours - previousApprovedHours; + if (delta < 3) { + throw new BadRequestException( + `Only ${Math.round(delta * 10) / 10} new hours recorded since last approval. You need at least 3 hours of new work.`, + ); + } + } catch (err) { + if (err instanceof BadRequestException) throw err; + // If Hackatime lookup fails, let it through — reviewer will verify + this.logger.warn(`Hackatime hours check failed for resubmit: ${err}`); + } + } + + // Move project back to unreviewed, mark as update + project.status = 'unreviewed'; + project.isUpdate = true; + await this.projectRepo.save(project); + + // Create a submission record + const submission = this.submissionRepo.create({ + projectId: project.id, + userId, + changeDescription: cleanDesc, + minHoursConfirmed: true, + status: 'unreviewed', + }); + await this.submissionRepo.save(submission); + + await this.auditLogService.log( + userId, + 'project_submitted', + `Resubmitted "${project.name}" for review`, + impersonatorName, + ); + + return { success: true, submissionId: submission.id }; + } + + /* ------------------------------------------------------------------ */ + /* Submissions for a project */ + /* ------------------------------------------------------------------ */ + + async getSubmissions(projectId: string, userId: string) { + const project = await this.projectRepo.findOne({ + where: { id: projectId, userId }, + select: ['id'], + }); + if (!project) throw new NotFoundException('Project not found'); + + return this.submissionRepo.find({ + where: { projectId }, + order: { createdAt: 'DESC' }, + select: ['id', 'changeDescription', 'minHoursConfirmed', 'status', 'createdAt'], + }); + } + + /* ------------------------------------------------------------------ */ + /* Project detail (public, single approved project) */ + /* ------------------------------------------------------------------ */ + + async findApprovedProjectById(projectId: string) { + const project = await this.projectRepo + .createQueryBuilder('project') + .innerJoinAndSelect('project.user', 'user') + .where('project.id = :id', { id: projectId }) + .andWhere('project.status = :status', { status: 'approved' }) + .select([ + 'project.id', + 'project.name', + 'project.description', + 'project.projectType', + 'project.screenshot1Url', + 'project.screenshot2Url', + 'project.codeUrl', + 'project.demoUrl', + 'project.hackatimeProjectName', + 'project.overrideHours', + 'user.id', + 'user.hcaSub', + 'user.name', + 'user.nickname', + 'user.hackatimeToken', + ]) + .getOne(); + + if (!project) return null; + + const names = (project.hackatimeProjectName ?? []).filter((n) => !!n); + let hours = 0; + if (project.overrideHours != null) { + hours = project.overrideHours; + } else if (names.length > 0 && project.user.hackatimeToken) { + try { + const result = await this.hackatimeService.getHoursForProjects( + project.user.hcaSub, + names, + ); + hours = result.hours; + } catch { /* graceful fallback */ } + } + + return { + id: project.id, + name: project.name, + description: project.description, + projectType: project.projectType, + screenshot1Url: project.screenshot1Url, + screenshot2Url: project.screenshot2Url, + codeUrl: project.codeUrl, + demoUrl: project.demoUrl, + hours, + builderName: project.user.nickname || project.user.name || 'Anonymous', + ownerId: project.user.id, + }; + } + + /* ------------------------------------------------------------------ */ + /* Comments */ + /* ------------------------------------------------------------------ */ + + async getComments(projectId: string) { + const comments = await this.commentRepo.find({ + where: { projectId }, + order: { createdAt: 'ASC' }, + relations: ['user'], + }); + + return comments.map((c) => ({ + id: c.id, + body: c.body, + authorName: c.user?.nickname || c.user?.name || 'Anonymous', + authorId: c.userId, + createdAt: c.createdAt, + })); + } + + async addComment(projectId: string, userId: string, body: string) { + // Verify the project exists and is approved + const project = await this.projectRepo.findOne({ + where: { id: projectId, status: 'approved' }, + select: ['id'], + }); + if (!project) throw new NotFoundException('Project not found'); + + // Sanitize and validate + const clean = this.sanitize(body).slice(0, 500); + if (clean.length === 0) { + throw new BadRequestException('Comment cannot be empty'); + } + + const comment = this.commentRepo.create({ + projectId, + userId, + body: clean, + }); + const saved = await this.commentRepo.save(comment); + + return { id: saved.id, body: saved.body, createdAt: saved.createdAt }; + } + + async deleteComment(commentId: string, userId: string, userEmail: string) { + const comment = await this.commentRepo.findOne({ + where: { id: commentId }, + relations: ['project'], + }); + if (!comment) throw new NotFoundException('Comment not found'); + + // Allow deletion by: comment author, project owner, or admin + const isAuthor = comment.userId === userId; + const isProjectOwner = comment.project?.userId === userId; + + if (!isAuthor && !isProjectOwner) { + // Check if user is admin + const perms = await this.rsvpService.getPerms(userEmail); + const isAdmin = perms && ['Super Admin', 'Reviewer', 'Fraud Reviewer'].includes(perms); + if (!isAdmin) { + throw new ForbiddenException('Not allowed to delete this comment'); + } + } + + await this.commentRepo.remove(comment); + return { deleted: true }; + } + + /* ------------------------------------------------------------------ */ + /* Sanitisation helpers */ + /* ------------------------------------------------------------------ */ + + /** + * Strips characters that could be used for HTML/SQL/script injection. + * The result is treated as a plain-text string. + */ + private sanitize(raw: string): string { + return String(raw) + .replace(/[<>"'`&\\]/g, '') // strip injection-relevant chars + .replace(/\0/g, '') // strip null bytes + .trim(); + } + + private requireString( + value: unknown, + field: string, + maxLen: number, + ): string { + if (!value || typeof value !== 'string') { + throw new BadRequestException(`${field} is required`); + } + const clean = this.sanitize(value).slice(0, maxLen); + if (clean.length === 0) { + throw new BadRequestException(`${field} is required`); + } + return clean; + } + + private validateOptionalString( + value: unknown, + field: string, + maxLen: number, + ): string | null { + if (!value || typeof value !== 'string') return null; + const clean = this.sanitize(value).slice(0, maxLen); + return clean.length === 0 ? null : clean; + } + + private validateProjectType(value: unknown): string { + if (!value || typeof value !== 'string') { + throw new BadRequestException('projectType is required'); + } + const v = value.trim().toLowerCase(); + if (!(VALID_PROJECT_TYPES as readonly string[]).includes(v)) { + throw new BadRequestException( + `projectType must be one of: ${VALID_PROJECT_TYPES.join(', ')}`, + ); + } + return v; + } + + /* ------------------------------------------------------------------ */ + /* URL validation */ + /* ------------------------------------------------------------------ */ + + /** Matches private/reserved IP ranges and localhost. */ + private static readonly BLOCKED_HOSTS = + /^(localhost|127\.|10\.|192\.168\.|172\.(1[6-9]|2\d|3[01])\.|169\.254\.|0\.|::1$|fc|fd|\[::1\])/i; + + private validateUrl( + value: string | undefined, + field: string, + ): string | null { + if (!value || typeof value !== 'string') return null; + const trimmed = value.trim(); + if (trimmed.length === 0) return null; + + if (!trimmed.startsWith('https://')) { + throw new BadRequestException( + `${field} must start with https:// — please prepend it to your link`, + ); + } + + if (trimmed.length > 2048) { + throw new BadRequestException(`${field} is too long (max 2048 chars)`); + } + + try { + const parsed = new URL(trimmed); + if (parsed.protocol !== 'https:') { + throw new Error(); + } + if (ProjectsService.BLOCKED_HOSTS.test(parsed.hostname)) { + throw new Error('Internal URL'); + } + } catch { + throw new BadRequestException(`${field} is not a valid URL`); + } + + return trimmed; + } + + /* ------------------------------------------------------------------ */ + /* Screenshot validation & CDN upload */ + /* ------------------------------------------------------------------ */ + + /** + * Validates base64 data URIs: checks MIME type, magic bytes, and size. + * Returns an array of { mime, buffer } for valid screenshots. + */ + private validateScreenshots( + screenshots: string[] | undefined, + ): { mime: string; buffer: Buffer }[] { + if (!screenshots || !Array.isArray(screenshots)) return []; + + const items = screenshots.slice(0, 2); + const results: { mime: string; buffer: Buffer }[] = []; + + for (let i = 0; i < items.length; i++) { + const raw = items[i]; + if (!raw || typeof raw !== 'string') continue; + + // Expect data URI format: data:image/...;base64,... + const match = raw.match( + /^data:(image\/(?:png|jpeg|gif|webp));base64,(.+)$/, + ); + if (!match) { + throw new BadRequestException( + `Screenshot ${i + 1} must be a PNG, JPEG, GIF, or WebP image`, + ); + } + + const declaredMime = match[1]; + const b64Data = match[2]; + + // Decode to check real size and magic bytes + const buffer = Buffer.from(b64Data, 'base64'); + + // Verify magic bytes match declared MIME type + const sig = IMAGE_SIGNATURES.find((s) => s.mime === declaredMime); + if (!sig || !b64Data.startsWith(sig.b64Prefix)) { + throw new BadRequestException( + `Screenshot ${i + 1} content does not match its declared type (${declaredMime})`, + ); + } + + results.push({ mime: declaredMime, buffer }); + } + + return results; + } + + /** + * Uploads validated screenshot buffers to the Hack Club CDN. + * Returns [url1 | null, url2 | null]. + */ + private async uploadScreenshots( + items: { mime: string; buffer: Buffer }[], + ): Promise<[string | null, string | null]> { + const urls: [string | null, string | null] = [null, null]; + + for (let i = 0; i < items.length; i++) { + const { mime, buffer } = items[i]; + const ext = MIME_EXTENSIONS[mime] ?? 'bin'; + const filename = `screenshot-${Date.now()}-${i + 1}.${ext}`; + + const blob = new Blob([new Uint8Array(buffer)], { type: mime }); + const formData = new FormData(); + formData.append('file', blob, filename); + + let res: Response; + try { + res = await fetchWithTimeout(CDN_UPLOAD_URL, { + method: 'POST', + headers: { Authorization: `Bearer ${this.cdnApiKey}` }, + body: formData, + }); + } catch (err) { + this.logger.error(`CDN upload network error for screenshot ${i + 1}: ${err}`); + throw new BadRequestException( + `Screenshot upload failed — the CDN is unreachable. Try again without a screenshot.`, + ); + } + + if (!res.ok) { + const err = await res.text().catch(() => ''); + this.logger.error(`CDN upload failed (${res.status}): ${err}`); + throw new BadRequestException( + `Failed to upload screenshot ${i + 1}. Please try again.`, + ); + } + + const data = await res.json().catch(() => null); + if (!data?.url) { + this.logger.error(`CDN upload returned no URL for screenshot ${i + 1}`); + throw new BadRequestException( + `Failed to upload screenshot ${i + 1}. Please try again.`, + ); + } + urls[i] = data.url; + } + + return urls; + } +} diff --git a/backend/src/projects/update-project.dto.ts b/backend/src/projects/update-project.dto.ts new file mode 100644 index 0000000..61de799 --- /dev/null +++ b/backend/src/projects/update-project.dto.ts @@ -0,0 +1,14 @@ +export class UpdateProjectDto { + name?: string; + description?: string; + projectType?: string; + codeUrl?: string | null; + readmeUrl?: string | null; + demoUrl?: string | null; + screenshots?: string[]; + hackatimeProjectName?: string[] | null; + isUpdate?: boolean; + otherHcProgram?: string | null; + aiUse?: string | null; + status?: string; +} diff --git a/backend/src/rsvp/rsvp.module.ts b/backend/src/rsvp/rsvp.module.ts new file mode 100644 index 0000000..02e7d97 --- /dev/null +++ b/backend/src/rsvp/rsvp.module.ts @@ -0,0 +1,8 @@ +import { Module } from '@nestjs/common'; +import { RsvpService } from './rsvp.service'; + +@Module({ + providers: [RsvpService], + exports: [RsvpService], +}) +export class RsvpModule {} diff --git a/backend/src/rsvp/rsvp.service.ts b/backend/src/rsvp/rsvp.service.ts new file mode 100644 index 0000000..5ac356b --- /dev/null +++ b/backend/src/rsvp/rsvp.service.ts @@ -0,0 +1,268 @@ +import { + Injectable, + BadRequestException, + HttpException, + HttpStatus, +} from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { fetchWithTimeout } from '../fetch.util'; + +const EMAIL_RE = + /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/; + +@Injectable() +export class RsvpService { + private readonly pendingEmails = new Set(); + private readonly airtableApiKey: string; + private readonly airtableBaseId: string; + private readonly airtableTableName: string; + + constructor(private readonly config: ConfigService) { + this.airtableApiKey = this.config.getOrThrow('AIRTABLE_API_KEY'); + this.airtableBaseId = this.config.getOrThrow('AIRTABLE_BASE_ID'); + this.airtableTableName = this.config.getOrThrow( + 'AIRTABLE_TABLE_NAME', + ); + } + + private sanitizeEmail(raw: string): string { + return raw.trim().slice(0, 254).replace(/[<>"'&\\]/g, ''); + } + + /** + * Escapes a string for safe use inside an Airtable filterByFormula value. + * Doubles any backslashes first, then escapes double-quotes. + * The value is wrapped in double-quotes by the caller. + */ + private escapeAirtableValue(value: string): string { + return value.replace(/\\/g, '\\\\').replace(/"/g, '\\"'); + } + + private get baseUrl(): string { + return `https://api.airtable.com/v0/${this.airtableBaseId}/${encodeURIComponent(this.airtableTableName)}`; + } + + async createRsvp(rawEmail: string): Promise<{ success: true; existing: boolean }> { + const email = this.sanitizeEmail(rawEmail); + + if (!EMAIL_RE.test(email)) { + throw new BadRequestException('Invalid email address'); + } + + if (this.pendingEmails.has(email)) { + throw new HttpException('RSVP already in progress', HttpStatus.TOO_MANY_REQUESTS); + } + this.pendingEmails.add(email); + + try { + const existing = await this.checkExisting(email); + if (existing) { + return { success: true, existing: true }; + } + + await this.createRecord(email); + return { success: true, existing: false }; + } finally { + this.pendingEmails.delete(email); + } + } + + private async checkExisting(email: string): Promise { + const searchParams = new URLSearchParams({ + filterByFormula: `{Email} = "${this.escapeAirtableValue(email)}"`, + maxRecords: '1', + }); + + const res = await fetchWithTimeout(`${this.baseUrl}?${searchParams}`, { + headers: { Authorization: `Bearer ${this.airtableApiKey}` }, + }); + + if (!res.ok) { + const text = await res.text(); + console.error('Airtable lookup error:', res.status, text); + throw new HttpException('Failed to check RSVP', HttpStatus.BAD_GATEWAY); + } + + const data = await res.json(); + return data.records?.length > 0; + } + + async findRecordIdByEmail(rawEmail: string): Promise { + const email = this.sanitizeEmail(rawEmail); + const searchParams = new URLSearchParams({ + filterByFormula: `{Email} = "${this.escapeAirtableValue(email)}"`, + maxRecords: '1', + }); + + const res = await fetchWithTimeout(`${this.baseUrl}?${searchParams}`, { + headers: { Authorization: `Bearer ${this.airtableApiKey}` }, + }); + + if (!res.ok) { + throw new HttpException('Failed to find Airtable record', HttpStatus.BAD_GATEWAY); + } + + const data = await res.json(); + return data.records?.[0]?.id ?? null; + } + + async updatePerms(rawEmail: string, perms: string): Promise { + const recordId = await this.findRecordIdByEmail(rawEmail); + if (!recordId) { + throw new HttpException('User not found in Airtable', HttpStatus.NOT_FOUND); + } + + const res = await fetchWithTimeout(`${this.baseUrl}/${recordId}`, { + method: 'PATCH', + headers: { + Authorization: `Bearer ${this.airtableApiKey}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ fields: { Perms: perms } }), + }); + + if (!res.ok) { + const text = await res.text(); + console.error('Airtable update error:', res.status, text); + throw new HttpException('Failed to update permissions', HttpStatus.BAD_GATEWAY); + } + } + + async getPerms(rawEmail: string): Promise { + const email = this.sanitizeEmail(rawEmail); + const searchParams = new URLSearchParams({ + filterByFormula: `{Email} = "${this.escapeAirtableValue(email)}"`, + maxRecords: '1', + }); + searchParams.append('fields[]', 'Perms'); + + const res = await fetchWithTimeout(`${this.baseUrl}?${searchParams}`, { + headers: { Authorization: `Bearer ${this.airtableApiKey}` }, + }); + + if (!res.ok) { + throw new HttpException( + 'Failed to check permissions', + HttpStatus.BAD_GATEWAY, + ); + } + + const data = await res.json(); + return data.records?.[0]?.fields?.Perms ?? null; + } + + async getAllPerms(): Promise> { + const permsMap = new Map(); + let offset: string | undefined; + + do { + const searchParams = new URLSearchParams(); + searchParams.append('fields[]', 'Email'); + searchParams.append('fields[]', 'Perms'); + searchParams.append('pageSize', '100'); + if (offset) searchParams.append('offset', offset); + + const res = await fetchWithTimeout(`${this.baseUrl}?${searchParams}`, { + headers: { Authorization: `Bearer ${this.airtableApiKey}` }, + }); + + if (!res.ok) break; + + const data = await res.json(); + for (const record of data.records ?? []) { + const email = record.fields?.Email; + const perms = record.fields?.Perms; + if (email) permsMap.set(email.toLowerCase(), perms ?? 'User'); + } + offset = data.offset; + } while (offset); + + return permsMap; + } + + /** + * Sets a date field in Airtable for Loops sync, only if not already set. + * Fire-and-forget — logs errors but never throws. + */ + async updateDateField(rawEmail: string, fieldName: string): Promise { + try { + const email = this.sanitizeEmail(rawEmail); + const searchParams = new URLSearchParams({ + filterByFormula: `{Email} = "${this.escapeAirtableValue(email)}"`, + maxRecords: '1', + }); + searchParams.append('fields[]', fieldName); + + const lookupRes = await fetchWithTimeout(`${this.baseUrl}?${searchParams}`, { + headers: { Authorization: `Bearer ${this.airtableApiKey}` }, + }); + if (!lookupRes.ok) return; + + const data = await lookupRes.json(); + const record = data.records?.[0]; + if (!record) return; + + // Skip if the field already has a value + if (record.fields?.[fieldName]) return; + + const today = new Date().toISOString().split('T')[0]; // YYYY-MM-DD + const res = await fetchWithTimeout(`${this.baseUrl}/${record.id}`, { + method: 'PATCH', + headers: { + Authorization: `Bearer ${this.airtableApiKey}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ fields: { [fieldName]: today } }), + }); + + if (!res.ok) { + const text = await res.text(); + console.error(`Airtable updateDateField(${fieldName}) error:`, res.status, text); + } + } catch (err) { + console.error(`Airtable updateDateField(${fieldName}) failed:`, err); + } + } + + async getStickerLink(rawEmail: string): Promise { + const email = this.sanitizeEmail(rawEmail); + const searchParams = new URLSearchParams({ + filterByFormula: `{Email} = "${this.escapeAirtableValue(email)}"`, + maxRecords: '1', + }); + searchParams.append('fields[]', 'Fillout Sticker Link'); + + const res = await fetchWithTimeout(`${this.baseUrl}?${searchParams}`, { + headers: { Authorization: `Bearer ${this.airtableApiKey}` }, + }); + + if (!res.ok) { + throw new HttpException( + 'Failed to fetch sticker link', + HttpStatus.BAD_GATEWAY, + ); + } + + const data = await res.json(); + return data.records?.[0]?.fields?.['Fillout Sticker Link'] ?? null; + } + + private async createRecord(email: string): Promise { + const res = await fetchWithTimeout(this.baseUrl, { + method: 'POST', + headers: { + Authorization: `Bearer ${this.airtableApiKey}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + records: [{ fields: { Email: email } }], + }), + }); + + if (!res.ok) { + const text = await res.text(); + console.error('Airtable error:', res.status, text); + throw new HttpException('Failed to save RSVP', HttpStatus.BAD_GATEWAY); + } + } +} diff --git a/backend/src/shop/shop.controller.ts b/backend/src/shop/shop.controller.ts new file mode 100644 index 0000000..91daffd --- /dev/null +++ b/backend/src/shop/shop.controller.ts @@ -0,0 +1,83 @@ +import { + Controller, + Get, + Post, + Body, + Req, + UseGuards, + BadRequestException, +} from '@nestjs/common'; +import type { Request } from 'express'; +import { JwtAuthGuard } from '../auth/jwt-auth.guard'; +import { ShopService } from './shop.service'; + +@Controller('api/shop') +export class ShopController { + constructor(private readonly shopService: ShopService) {} + + @UseGuards(JwtAuthGuard) + @Get() + async list() { + return this.shopService.listActive(); + } + + @UseGuards(JwtAuthGuard) + @Post('purchase') + async purchase( + @Req() req: Request, + @Body() body: { shopItemId?: string; quantity?: number }, + ) { + const userId = (req as any).user?.uid; + if (!userId) throw new BadRequestException('Not authenticated'); + if (!body.shopItemId || typeof body.shopItemId !== 'string') { + throw new BadRequestException('shopItemId is required'); + } + if (body.quantity === undefined || !Number.isInteger(body.quantity) || body.quantity < 1) { + throw new BadRequestException('quantity must be a positive integer'); + } + return this.shopService.purchase(userId, body.shopItemId, body.quantity); + } + + @UseGuards(JwtAuthGuard) + @Get('pipes') + async getPipes(@Req() req: Request) { + const userId = (req as any).user?.uid; + if (!userId) throw new BadRequestException('Not authenticated'); + const pipes = await this.shopService.getPipes(userId); + return { pipes }; + } + + @UseGuards(JwtAuthGuard) + @Get('orders') + async getOrders(@Req() req: Request) { + const userId = (req as any).user?.uid; + if (!userId) throw new BadRequestException('Not authenticated'); + return this.shopService.getUserOrders(userId); + } + + @UseGuards(JwtAuthGuard) + @Get('fulfillment') + async getFulfillmentUpdates(@Req() req: Request) { + const userId = (req as any).user?.uid; + if (!userId) throw new BadRequestException('Not authenticated'); + return this.shopService.getUserFulfillmentUpdates(userId); + } + + @UseGuards(JwtAuthGuard) + @Post('fulfillment/read') + async markRead(@Req() req: Request) { + const userId = (req as any).user?.uid; + if (!userId) throw new BadRequestException('Not authenticated'); + await this.shopService.markUpdatesRead(userId); + return { success: true }; + } + + @UseGuards(JwtAuthGuard) + @Get('fulfillment/unread') + async getUnreadCount(@Req() req: Request) { + const userId = (req as any).user?.uid; + if (!userId) throw new BadRequestException('Not authenticated'); + const count = await this.shopService.getUnreadCount(userId); + return { count }; + } +} diff --git a/backend/src/shop/shop.module.ts b/backend/src/shop/shop.module.ts new file mode 100644 index 0000000..549744a --- /dev/null +++ b/backend/src/shop/shop.module.ts @@ -0,0 +1,24 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { AuthModule } from '../auth/auth.module'; +import { AuditLogModule } from '../audit-log/audit-log.module'; +import { RsvpModule } from '../rsvp/rsvp.module'; +import { ShopItem } from '../entities/shop-item.entity'; +import { Order } from '../entities/order.entity'; +import { FulfillmentUpdate } from '../entities/fulfillment-update.entity'; +import { User } from '../entities/user.entity'; +import { ShopController } from './shop.controller'; +import { ShopService } from './shop.service'; + +@Module({ + imports: [ + TypeOrmModule.forFeature([ShopItem, Order, FulfillmentUpdate, User]), + AuthModule, + AuditLogModule, + RsvpModule, + ], + controllers: [ShopController], + providers: [ShopService], + exports: [ShopService], +}) +export class ShopModule {} diff --git a/backend/src/shop/shop.service.ts b/backend/src/shop/shop.service.ts new file mode 100644 index 0000000..205d8f2 --- /dev/null +++ b/backend/src/shop/shop.service.ts @@ -0,0 +1,316 @@ +import { + Injectable, + BadRequestException, + NotFoundException, + ConflictException, + Logger, +} from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository, DataSource } from 'typeorm'; +import { ShopItem } from '../entities/shop-item.entity'; +import { Order } from '../entities/order.entity'; +import { FulfillmentUpdate } from '../entities/fulfillment-update.entity'; +import { User } from '../entities/user.entity'; +import { AuditLogService } from '../audit-log/audit-log.service'; +import { RsvpService } from '../rsvp/rsvp.service'; + +@Injectable() +export class ShopService { + private readonly logger = new Logger(ShopService.name); + + constructor( + @InjectRepository(ShopItem) + private readonly shopRepo: Repository, + @InjectRepository(Order) + private readonly orderRepo: Repository, + @InjectRepository(FulfillmentUpdate) + private readonly fulfillmentRepo: Repository, + @InjectRepository(User) + private readonly userRepo: Repository, + private readonly dataSource: DataSource, + private readonly auditLogService: AuditLogService, + private readonly rsvpService: RsvpService, + ) {} + + async listActive() { + return this.shopRepo.find({ + where: { isActive: true }, + order: { sortOrder: 'ASC' }, + select: ['id', 'name', 'description', 'imageUrl', 'priceHours', 'stock', 'sortOrder', 'estimatedShip'], + }); + } + + /** + * Purchase a shop item. Uses a serializable transaction with pessimistic + * locking on both the user row and the shop item row to prevent race + * conditions (double-spend, overselling). + */ + async purchase(userId: string, shopItemId: string, quantity: number) { + // Validate quantity upfront + if (!Number.isInteger(quantity) || quantity < 1) { + throw new BadRequestException('Quantity must be a positive integer'); + } + if (quantity > 100) { + throw new BadRequestException('Maximum quantity per order is 100'); + } + + return this.dataSource.transaction('SERIALIZABLE', async (manager) => { + // Lock the user row + const user = await manager.findOne(User, { + where: { id: userId }, + lock: { mode: 'pessimistic_write' }, + }); + if (!user) throw new NotFoundException('User not found'); + + // Lock the shop item row + const item = await manager.findOne(ShopItem, { + where: { id: shopItemId, isActive: true }, + lock: { mode: 'pessimistic_write' }, + }); + if (!item) throw new NotFoundException('Shop item not found or inactive'); + + // Check stock + if (item.stock !== null) { + if (item.stock < quantity) { + throw new ConflictException( + item.stock === 0 + ? 'This item is out of stock' + : `Only ${item.stock} remaining`, + ); + } + } + + // Check budget (pipes) + const totalCost = item.priceHours * quantity; + if (user.pipes < totalCost) { + throw new BadRequestException( + `Not enough Pipes. You have ${user.pipes}, need ${totalCost}`, + ); + } + + // Deduct pipes + user.pipes -= totalCost; + await manager.save(User, user); + + // Deduct stock if limited + if (item.stock !== null) { + item.stock -= quantity; + // If stock hits 0, deactivate the item + if (item.stock <= 0) { + item.isActive = false; + } + await manager.save(ShopItem, item); + } + + // Create order + const order = manager.create(Order, { + userId, + shopItemId: item.id, + quantity, + pipesSpent: totalCost, + itemName: item.name, + status: 'pending', + }); + const savedOrder = await manager.save(Order, order); + + // Create fulfillment update + const update = manager.create(FulfillmentUpdate, { + userId, + orderId: savedOrder.id, + message: 'Hey! we got the order - I\'ll keep you updated on when I get it fulfilled.', + isRead: false, + }); + await manager.save(FulfillmentUpdate, update); + + return { + orderId: savedOrder.id, + itemName: item.name, + quantity, + pipesSpent: totalCost, + remainingPipes: user.pipes, + }; + }).then(async (result) => { + // Audit log outside the transaction + await this.auditLogService.log( + userId, + 'shop_purchase', + `Purchased ${result.quantity}x ${result.itemName} for ${result.pipesSpent} Pipes`, + ); + + // Sync purchase date to Airtable for Loops + this.userRepo.findOne({ where: { id: userId }, select: ['email'] }).then((u) => { + if (u?.email) this.rsvpService.updateDateField(u.email, 'Loops - beestPurchasedItem'); + }); + + return result; + }); + } + + /** Get user's pipes balance */ + async getPipes(userId: string): Promise { + const user = await this.userRepo.findOne({ + where: { id: userId }, + select: ['id', 'pipes'], + }); + return user?.pipes ?? 0; + } + + /** Get orders for a specific user */ + async getUserOrders(userId: string) { + const orders = await this.orderRepo.find({ + where: { userId }, + order: { createdAt: 'DESC' }, + select: ['id', 'itemName', 'quantity', 'pipesSpent', 'status', 'createdAt'], + }); + return orders; + } + + /** Get fulfillment updates for a user */ + async getUserFulfillmentUpdates(userId: string) { + const updates = await this.fulfillmentRepo.find({ + where: { userId }, + order: { createdAt: 'DESC' }, + relations: ['order'], + }); + return updates.map((u) => ({ + id: u.id, + orderId: u.orderId, + itemName: u.order?.itemName ?? 'Unknown', + message: u.message, + isRead: u.isRead, + createdAt: u.createdAt, + })); + } + + /** Mark all fulfillment updates as read for a user */ + async markUpdatesRead(userId: string) { + await this.fulfillmentRepo.update({ userId, isRead: false }, { isRead: true }); + } + + /** Count unread fulfillment updates */ + async getUnreadCount(userId: string): Promise { + return this.fulfillmentRepo.count({ where: { userId, isRead: false } }); + } + + // ── Admin methods ── + + /** List all orders with filtering and sorting */ + async listAllOrders(options?: { + shopItemId?: string; + status?: string; + sortBy?: 'oldest' | 'newest'; + }) { + const qb = this.orderRepo + .createQueryBuilder('order') + .leftJoinAndSelect('order.user', 'user') + .select([ + 'order.id', + 'order.userId', + 'order.shopItemId', + 'order.itemName', + 'order.quantity', + 'order.pipesSpent', + 'order.status', + 'order.createdAt', + 'order.updatedAt', + 'user.id', + 'user.name', + 'user.nickname', + 'user.slackId', + ]); + + if (options?.shopItemId) { + qb.andWhere('order.shopItemId = :shopItemId', { + shopItemId: options.shopItemId, + }); + } + + if (options?.status) { + qb.andWhere('order.status = :status', { status: options.status }); + } + + if (options?.sortBy === 'oldest') { + qb.orderBy('order.createdAt', 'ASC'); + } else { + qb.orderBy('order.createdAt', 'DESC'); + } + + const orders = await qb.getMany(); + + return orders.map((o) => ({ + id: o.id, + userId: o.userId, + shopItemId: o.shopItemId, + itemName: o.itemName, + quantity: o.quantity, + pipesSpent: o.pipesSpent, + status: o.status, + createdAt: o.createdAt, + updatedAt: o.updatedAt, + userName: o.user?.nickname || o.user?.name || 'Unknown', + userSlackId: o.user?.slackId || null, + pendingSince: o.status === 'pending' + ? Math.floor((Date.now() - new Date(o.createdAt).getTime()) / (1000 * 60 * 60)) + : null, + })); + } + + /** Mark an order as fulfilled — uses pessimistic lock to prevent double-fulfill */ + async fulfillOrder(orderId: string) { + return this.dataSource.transaction('SERIALIZABLE', async (manager) => { + const order = await manager.findOne(Order, { + where: { id: orderId }, + lock: { mode: 'pessimistic_write' }, + }); + if (!order) throw new NotFoundException('Order not found'); + if (order.status === 'fulfilled') { + throw new BadRequestException('Order is already fulfilled'); + } + + order.status = 'fulfilled'; + await manager.save(Order, order); + + const update = manager.create(FulfillmentUpdate, { + userId: order.userId, + orderId: order.id, + message: "Hey! I've sent out your order, its on the way to you :)", + isRead: false, + }); + await manager.save(FulfillmentUpdate, update); + + return order; + }).then(async (order) => { + await this.auditLogService.log( + order.userId, + 'order_fulfilled', + `Order for ${order.quantity}x ${order.itemName} was fulfilled`, + ); + + // Sync fulfillment date to Airtable for Loops + this.userRepo.findOne({ where: { id: order.userId }, select: ['email'] }).then((u) => { + if (u?.email) this.rsvpService.updateDateField(u.email, 'Loops - beestFulfilledOrder'); + }); + + return { success: true }; + }); + } + + /** Send a custom fulfillment message */ + async sendFulfillmentMessage(orderId: string, message: string) { + const order = await this.orderRepo.findOne({ where: { id: orderId } }); + if (!order) throw new NotFoundException('Order not found'); + + const clean = message.replace(/[<>"`&\\]/g, '').replace(/\0/g, '').trim().slice(0, 500); + if (!clean) throw new BadRequestException('Message cannot be empty'); + + const update = this.fulfillmentRepo.create({ + userId: order.userId, + orderId: order.id, + message: clean, + isRead: false, + }); + await this.fulfillmentRepo.save(update); + + return { success: true }; + } +} diff --git a/backend/src/slack/slack.module.ts b/backend/src/slack/slack.module.ts new file mode 100644 index 0000000..e6ab4a3 --- /dev/null +++ b/backend/src/slack/slack.module.ts @@ -0,0 +1,8 @@ + import { Module } from '@nestjs/common'; +import { SlackService } from './slack.service'; + +@Module({ + providers: [SlackService], + exports: [SlackService], +}) +export class SlackModule {} diff --git a/backend/src/slack/slack.service.ts b/backend/src/slack/slack.service.ts new file mode 100644 index 0000000..5fc1c78 --- /dev/null +++ b/backend/src/slack/slack.service.ts @@ -0,0 +1,53 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { fetchWithTimeout } from '../fetch.util'; + +export type SlackMembershipStatus = 'full_member' | 'guest' | 'not_found'; + +@Injectable() +export class SlackService { + private readonly logger = new Logger(SlackService.name); + private readonly botToken: string | undefined; + private readonly configured: boolean; + + constructor(private configService: ConfigService) { + this.botToken = this.configService.get('SLACK_BOT_TOKEN'); + this.configured = !!this.botToken; + if (!this.configured) { + this.logger.warn('SLACK_BOT_TOKEN not set — Slack membership checks disabled'); + } + } + + async checkMembership(email: string): Promise { + if (!this.configured) { + throw new Error('Slack integration is not configured'); + } + + const res = await fetchWithTimeout( + `https://slack.com/api/users.lookupByEmail?email=${encodeURIComponent(email)}`, + { headers: { Authorization: `Bearer ${this.botToken}` } }, + ); + + if (!res.ok) { + this.logger.error(`Slack API HTTP error: ${res.status}`); + throw new Error('Slack API request failed'); + } + + const data = await res.json(); + + if (!data.ok) { + if (data.error === 'users_not_found') { + return 'not_found'; + } + this.logger.error(`Slack API error: ${data.error}`); + throw new Error(`Slack API error: ${data.error}`); + } + + const user = data.user; + if (user.is_restricted || user.is_ultra_restricted) { + return 'guest'; + } + + return 'full_member'; + } +} diff --git a/backend/test/app.e2e-spec.ts b/backend/test/app.e2e-spec.ts new file mode 100644 index 0000000..36852c5 --- /dev/null +++ b/backend/test/app.e2e-spec.ts @@ -0,0 +1,25 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { INestApplication } from '@nestjs/common'; +import request from 'supertest'; +import { App } from 'supertest/types'; +import { AppModule } from './../src/app.module'; + +describe('AppController (e2e)', () => { + let app: INestApplication; + + beforeEach(async () => { + const moduleFixture: TestingModule = await Test.createTestingModule({ + imports: [AppModule], + }).compile(); + + app = moduleFixture.createNestApplication(); + await app.init(); + }); + + it('/ (GET)', () => { + return request(app.getHttpServer()) + .get('/') + .expect(200) + .expect('Hello World!'); + }); +}); diff --git a/backend/test/jest-e2e.json b/backend/test/jest-e2e.json new file mode 100644 index 0000000..e9d912f --- /dev/null +++ b/backend/test/jest-e2e.json @@ -0,0 +1,9 @@ +{ + "moduleFileExtensions": ["js", "json", "ts"], + "rootDir": ".", + "testEnvironment": "node", + "testRegex": ".e2e-spec.ts$", + "transform": { + "^.+\\.(t|j)s$": "ts-jest" + } +} diff --git a/backend/tsconfig.build.json b/backend/tsconfig.build.json new file mode 100644 index 0000000..64f86c6 --- /dev/null +++ b/backend/tsconfig.build.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] +} diff --git a/backend/tsconfig.json b/backend/tsconfig.json new file mode 100644 index 0000000..57f9635 --- /dev/null +++ b/backend/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "module": "nodenext", + "moduleResolution": "nodenext", + "resolvePackageJsonExports": true, + "esModuleInterop": true, + "isolatedModules": true, + "declaration": true, + "removeComments": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "allowSyntheticDefaultImports": true, + "target": "ES2023", + "sourceMap": true, + "outDir": "./dist", + "baseUrl": "./", + "incremental": true, + "skipLibCheck": true, + "strictNullChecks": true, + "forceConsistentCasingInFileNames": true, + "noImplicitAny": true, + "strictBindCallApply": true, + "noFallthroughCasesInSwitch": true + } +} diff --git a/beest/.vscode/extensions.json b/beest/.vscode/extensions.json deleted file mode 100644 index 38f928a..0000000 --- a/beest/.vscode/extensions.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "recommendations": ["svelte.svelte-vscode", "esbenp.prettier-vscode"] -} diff --git a/beest/CLAUDE.md b/beest/CLAUDE.md deleted file mode 100644 index 55be113..0000000 --- a/beest/CLAUDE.md +++ /dev/null @@ -1,77 +0,0 @@ -## Project Configuration - -- **Language**: TypeScript -- **Package Manager**: npm -- **Add-ons**: prettier, mcp -- **Framework**: SvelteKit (Svelte 5 with runes) -- **Entry point**: `src/routes/+page.svelte` — single-page site, all markup and styles in one file - ---- - -You are able to use the Svelte MCP server, where you have access to comprehensive Svelte 5 and SvelteKit documentation. Here's how to use the available tools effectively: - -## Available MCP Tools: - -### 1. list-sections - -Use this FIRST to discover all available documentation sections. Returns a structured list with titles, use_cases, and paths. -When asked about Svelte or SvelteKit topics, ALWAYS use this tool at the start of the chat to find relevant sections. - -### 2. get-documentation - -Retrieves full documentation content for specific sections. Accepts single or multiple sections. -After calling the list-sections tool, you MUST analyze the returned documentation sections (especially the use_cases field) and then use the get-documentation tool to fetch ALL documentation sections that are relevant for the user's task. - -### 3. svelte-autofixer - -Analyzes Svelte code and returns issues and suggestions. -You MUST use this tool whenever writing Svelte code before sending it to the user. Keep calling it until no issues or suggestions are returned. - -### 4. playground-link - -Generates a Svelte Playground link with the provided code. -After completing the code, ask the user if they want a playground link. Only call this tool after user confirmation and NEVER if code was written to files in their project. - -## Design Rules - -- Only use colors from the commented color palette in `src/routes/+page.svelte` for any UI work. -- `filter: saturate(1.5)` is applied to the body — all colors appear more vivid than their hex values suggest. Account for this when picking colors. -- A tileable rock texture (`/images/tile.webp`) is overlaid on all content sections via `::after` pseudo-elements at low opacity with `mix-blend-mode: overlay`. New sections need to be added to the texture selector list in the ` diff --git a/beest/src/routes/api/rsvp/+server.ts b/beest/src/routes/api/rsvp/+server.ts deleted file mode 100644 index 185e05c..0000000 --- a/beest/src/routes/api/rsvp/+server.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { json } from '@sveltejs/kit'; -import { AIRTABLE_API_KEY, AIRTABLE_BASE_ID, AIRTABLE_TABLE_NAME } from '$env/static/private'; -import type { RequestHandler } from './$types'; - -const EMAIL_RE = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/; - -const pendingEmails = new Set(); - -function sanitizeEmail(raw: string): string { - return raw.trim().slice(0, 254).replace(/[<>"'&\\]/g, ''); -} - -export const POST: RequestHandler = async ({ request }) => { - let body: unknown; - try { - body = await request.json(); - } catch { - return json({ error: 'Invalid JSON' }, { status: 400 }); - } - - const { email: rawEmail } = body as { email?: string }; - - if (!rawEmail || typeof rawEmail !== 'string') { - return json({ error: 'Email is required' }, { status: 400 }); - } - - const email = sanitizeEmail(rawEmail); - - if (!EMAIL_RE.test(email)) { - return json({ error: 'Invalid email address' }, { status: 400 }); - } - - if (pendingEmails.has(email)) { - return json({ error: 'RSVP already in progress' }, { status: 429 }); - } - pendingEmails.add(email); - - try { - // Check if the email already exists in Airtable - const searchParams = new URLSearchParams({ - filterByFormula: `{Email} = '${email.replace(/'/g, "\\'")}'`, - maxRecords: '1' - }); - const checkRes = await fetch( - `https://api.airtable.com/v0/${AIRTABLE_BASE_ID}/${encodeURIComponent(AIRTABLE_TABLE_NAME)}?${searchParams}`, - { - headers: { Authorization: `Bearer ${AIRTABLE_API_KEY}` } - } - ); - - if (!checkRes.ok) { - const text = await checkRes.text(); - console.error('Airtable lookup error:', checkRes.status, text); - return json({ error: 'Failed to check RSVP' }, { status: 502 }); - } - - const existing = await checkRes.json(); - if (existing.records && existing.records.length > 0) { - return json({ success: true, existing: true }); - } - - // Create new record - const res = await fetch(`https://api.airtable.com/v0/${AIRTABLE_BASE_ID}/${encodeURIComponent(AIRTABLE_TABLE_NAME)}`, { - method: 'POST', - headers: { - Authorization: `Bearer ${AIRTABLE_API_KEY}`, - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - records: [ - { - fields: { - Email: email - } - } - ] - }) - }); - - if (!res.ok) { - const text = await res.text(); - console.error('Airtable error:', res.status, text); - return json({ error: 'Failed to save RSVP' }, { status: 502 }); - } - - return json({ success: true, existing: false }); - } finally { - pendingEmails.delete(email); - } -}; diff --git a/beest/static/images/FAQ-Header.png b/beest/static/images/FAQ-Header.png deleted file mode 100644 index fbb77d4..0000000 Binary files a/beest/static/images/FAQ-Header.png and /dev/null differ diff --git a/beest/static/images/FAQ-Header.webp b/beest/static/images/FAQ-Header.webp deleted file mode 100644 index 8400ec9..0000000 Binary files a/beest/static/images/FAQ-Header.webp and /dev/null differ diff --git a/beest/static/images/frames/75 teens at Campfire Flagship.png b/beest/static/images/frames/75 teens at Campfire Flagship.png deleted file mode 100644 index 2672c16..0000000 Binary files a/beest/static/images/frames/75 teens at Campfire Flagship.png and /dev/null differ diff --git a/beest/static/images/frames/Hackathon on an island.png b/beest/static/images/frames/Hackathon on an island.png deleted file mode 100644 index d31bf9e..0000000 Binary files a/beest/static/images/frames/Hackathon on an island.png and /dev/null differ diff --git a/beest/static/images/frames/Teen hackers at Assemble.png b/beest/static/images/frames/Teen hackers at Assemble.png deleted file mode 100644 index 597a3f8..0000000 Binary files a/beest/static/images/frames/Teen hackers at Assemble.png and /dev/null differ diff --git a/beest/static/images/frames/Teens at a local game Jam.png b/beest/static/images/frames/Teens at a local game Jam.png deleted file mode 100644 index 19faf23..0000000 Binary files a/beest/static/images/frames/Teens at a local game Jam.png and /dev/null differ diff --git a/beest/static/images/frames/Winners of Parthenon Hackathon.png b/beest/static/images/frames/Winners of Parthenon Hackathon.png deleted file mode 100644 index f75683a..0000000 Binary files a/beest/static/images/frames/Winners of Parthenon Hackathon.png and /dev/null differ diff --git a/beest/static/images/frames/hackers debugging together.png b/beest/static/images/frames/hackers debugging together.png deleted file mode 100644 index bfa0f7e..0000000 Binary files a/beest/static/images/frames/hackers debugging together.png and /dev/null differ diff --git a/beest/static/images/hero.png b/beest/static/images/hero.png deleted file mode 100644 index 9bbae07..0000000 Binary files a/beest/static/images/hero.png and /dev/null differ diff --git a/beest/static/images/shop/poster.png b/beest/static/images/shop/poster.png deleted file mode 100644 index d10d05c..0000000 Binary files a/beest/static/images/shop/poster.png and /dev/null differ diff --git a/beest/static/images/sticker.png b/beest/static/images/sticker.png deleted file mode 100644 index 39ebfce..0000000 Binary files a/beest/static/images/sticker.png and /dev/null differ diff --git a/beest/static/images/sticker.webp b/beest/static/images/sticker.webp deleted file mode 100644 index 6221ed1..0000000 Binary files a/beest/static/images/sticker.webp and /dev/null differ diff --git a/beest/static/images/tile.png b/beest/static/images/tile.png deleted file mode 100644 index 630e961..0000000 Binary files a/beest/static/images/tile.png and /dev/null differ diff --git a/beest/static/images/tile.webp b/beest/static/images/tile.webp deleted file mode 100644 index e8da774..0000000 Binary files a/beest/static/images/tile.webp and /dev/null differ diff --git a/beest/.gitignore b/frontend/.gitignore similarity index 100% rename from beest/.gitignore rename to frontend/.gitignore diff --git a/beest/.mcp.json b/frontend/.mcp.json similarity index 100% rename from beest/.mcp.json rename to frontend/.mcp.json diff --git a/beest/.node-version b/frontend/.node-version similarity index 100% rename from beest/.node-version rename to frontend/.node-version diff --git a/beest/.npmrc b/frontend/.npmrc similarity index 100% rename from beest/.npmrc rename to frontend/.npmrc diff --git a/beest/.prettierignore b/frontend/.prettierignore similarity index 100% rename from beest/.prettierignore rename to frontend/.prettierignore diff --git a/beest/.prettierrc b/frontend/.prettierrc similarity index 100% rename from beest/.prettierrc rename to frontend/.prettierrc diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..e82ad1c --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,15 @@ +FROM node:22-alpine AS build +WORKDIR /app +COPY package*.json ./ +RUN npm ci +COPY . . +RUN npm run build + +FROM node:22-alpine +WORKDIR /app +COPY --from=build /app/build ./build +COPY --from=build /app/package*.json ./ +RUN npm ci --omit=dev +EXPOSE 3000 +ENV BODY_SIZE_LIMIT=10485760 +CMD ["node", "build"] diff --git a/beest/README.md b/frontend/README.md similarity index 100% rename from beest/README.md rename to frontend/README.md diff --git a/beest/package-lock.json b/frontend/package-lock.json similarity index 97% rename from beest/package-lock.json rename to frontend/package-lock.json index 099fc89..3de5b86 100644 --- a/beest/package-lock.json +++ b/frontend/package-lock.json @@ -7,17 +7,24 @@ "": { "name": "beest", "version": "0.0.1", + "dependencies": { + "canvas-confetti": "^1.9.4" + }, "devDependencies": { "@sveltejs/adapter-auto": "^7.0.0", "@sveltejs/adapter-node": "^5.5.4", "@sveltejs/kit": "^2.50.2", "@sveltejs/vite-plugin-svelte": "^6.2.4", + "@types/canvas-confetti": "^1.9.0", "prettier": "^3.8.1", "prettier-plugin-svelte": "^3.4.1", "svelte": "^5.51.0", "svelte-check": "^4.4.2", "typescript": "^5.9.3", - "vite": "^7.3.1" + "vite": "^7.3.2" + }, + "engines": { + "node": ">=22.12" } }, "node_modules/@esbuild/aix-ppc64": { @@ -1099,6 +1106,13 @@ "vite": "^6.3.0 || ^7.0.0" } }, + "node_modules/@types/canvas-confetti": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@types/canvas-confetti/-/canvas-confetti-1.9.0.tgz", + "integrity": "sha512-aBGj/dULrimR1XDZLtG9JwxX1b4HPRF6CX9Yfwh3NvstZEm1ZL7RBnel4keCPSqs1ANRu1u2Aoz9R+VmtjYuTg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/cookie": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", @@ -1174,6 +1188,16 @@ "node": ">= 0.4" } }, + "node_modules/canvas-confetti": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/canvas-confetti/-/canvas-confetti-1.9.4.tgz", + "integrity": "sha512-yxQbJkAVrFXWNbTUjPqjF7G+g6pDotOUHGbkZq2NELZUMDpiJ85rIEazVb8GTaAptNW2miJAXbs1BtioA251Pw==", + "license": "ISC", + "funding": { + "type": "donate", + "url": "https://www.paypal.me/kirilvatev" + } + }, "node_modules/chokidar": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", @@ -1482,9 +1506,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", "engines": { @@ -1782,9 +1806,9 @@ } }, "node_modules/vite": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", - "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.2.tgz", + "integrity": "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==", "dev": true, "license": "MIT", "dependencies": { diff --git a/beest/package.json b/frontend/package.json similarity index 88% rename from beest/package.json rename to frontend/package.json index eabbe89..a8409cf 100644 --- a/beest/package.json +++ b/frontend/package.json @@ -22,11 +22,15 @@ "@sveltejs/adapter-node": "^5.5.4", "@sveltejs/kit": "^2.50.2", "@sveltejs/vite-plugin-svelte": "^6.2.4", + "@types/canvas-confetti": "^1.9.0", "prettier": "^3.8.1", "prettier-plugin-svelte": "^3.4.1", "svelte": "^5.51.0", "svelte-check": "^4.4.2", "typescript": "^5.9.3", - "vite": "^7.3.1" + "vite": "^7.3.2" + }, + "dependencies": { + "canvas-confetti": "^1.9.4" } } diff --git a/beest/src/app.d.ts b/frontend/src/app.d.ts similarity index 100% rename from beest/src/app.d.ts rename to frontend/src/app.d.ts diff --git a/beest/src/app.html b/frontend/src/app.html similarity index 61% rename from beest/src/app.html rename to frontend/src/app.html index 80e7670..096478c 100644 --- a/beest/src/app.html +++ b/frontend/src/app.html @@ -3,8 +3,10 @@ + + %sveltekit.head% diff --git a/beest/src/lib/assets/favicon.svg b/frontend/src/lib/assets/favicon.svg similarity index 100% rename from beest/src/lib/assets/favicon.svg rename to frontend/src/lib/assets/favicon.svg diff --git a/beest/src/lib/index.ts b/frontend/src/lib/index.ts similarity index 100% rename from beest/src/lib/index.ts rename to frontend/src/lib/index.ts diff --git a/frontend/src/lib/server/auth.ts b/frontend/src/lib/server/auth.ts new file mode 100644 index 0000000..b4c5336 --- /dev/null +++ b/frontend/src/lib/server/auth.ts @@ -0,0 +1,149 @@ +import { env } from '$env/dynamic/private'; +import type { Cookies } from '@sveltejs/kit'; + +const BACKEND_URL = env.BACKEND_URL ?? 'http://localhost:3001'; + +const COOKIE_OPTS = { + path: '/', + httpOnly: true, + sameSite: 'lax' as const, + secure: env.NODE_ENV === 'production' +}; + +/** + * Attempts to refresh the auth token using the refresh token cookie. + * Returns the new token on success, null if refresh fails or no refresh token exists. + * Sets new cookies when a refresh occurs. + */ +export async function tryRefreshToken( + cookies: Cookies +): Promise { + const refreshTok = cookies.get('refresh_token'); + if (!refreshTok) { + cookies.delete('auth_token', { path: '/' }); + return null; + } + + const res = await fetch(`${BACKEND_URL}/api/auth/refresh`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ refreshToken: refreshTok }) + }); + + if (!res.ok) { + cookies.delete('auth_token', { path: '/' }); + cookies.delete('refresh_token', { path: '/' }); + return null; + } + + const data = await res.json(); + cookies.set('auth_token', data.token, { ...COOKIE_OPTS, maxAge: 3600 }); + cookies.set('refresh_token', data.refreshToken, { + ...COOKIE_OPTS, + maxAge: 90 * 24 * 60 * 60 + }); + return data.token; +} + +/** + * Proxies a request to the backend with auth. If the backend returns 401, + * attempts a token refresh and retries once. + */ +export async function proxyWithRefresh( + cookies: Cookies, + backendUrl: string, + init?: RequestInit +): Promise { + let token = cookies.get('auth_token'); + if (!token) { + token = await tryRefreshToken(cookies) ?? undefined; + if (!token) { + return new Response(JSON.stringify({ error: 'Not authenticated' }), { + status: 401, + headers: { 'Content-Type': 'application/json' } + }); + } + } + + let res = await fetch(backendUrl, { + ...init, + headers: { ...init?.headers, Authorization: `Bearer ${token}` } + }); + + if (res.status === 401) { + const newToken = await tryRefreshToken(cookies); + if (!newToken) { + return new Response(JSON.stringify({ error: 'Not authenticated' }), { + status: 401, + headers: { 'Content-Type': 'application/json' } + }); + } + res = await fetch(backendUrl, { + ...init, + headers: { ...init?.headers, Authorization: `Bearer ${newToken}` } + }); + } + + const data = await res.json().catch(() => ({})); + + // If the backend issued a new JWT (e.g. after nickname update), persist it + if (res.ok && data.token) { + cookies.set('auth_token', data.token, { ...COOKIE_OPTS, maxAge: 3600 }); + } + + return new Response(JSON.stringify(data), { + status: res.status, + headers: { 'Content-Type': 'application/json' } + }); +} + +/** + * Tries to authenticate the user via JWT, falling back to refresh token. + * Returns user claims on success, null on failure (both tokens expired). + * Transparently sets new cookies when a refresh occurs. + */ +export async function getAuthenticatedUser( + cookies: Cookies +): Promise | null> { + const token = cookies.get('auth_token'); + const refreshToken = cookies.get('refresh_token'); + + // 1. Try the JWT + if (token) { + const res = await fetch(`${BACKEND_URL}/api/auth/me`, { + headers: { Authorization: `Bearer ${token}` } + }); + if (res.ok) return res.json(); + } + + // 2. JWT expired or missing — try refresh + if (refreshToken) { + const res = await fetch(`${BACKEND_URL}/api/auth/refresh`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ refreshToken }) + }); + + if (res.ok) { + const data = await res.json(); + + // Set the rotated tokens + cookies.set('auth_token', data.token, { ...COOKIE_OPTS, maxAge: 3600 }); + cookies.set('refresh_token', data.refreshToken, { + ...COOKIE_OPTS, + maxAge: 90 * 24 * 60 * 60 + }); + + // Fetch user claims with the new JWT + const meRes = await fetch(`${BACKEND_URL}/api/auth/me`, { + headers: { Authorization: `Bearer ${data.token}` } + }); + if (meRes.ok) return meRes.json(); + } + } + + // 3. Both expired — clean up + cookies.delete('auth_token', { path: '/' }); + cookies.delete('refresh_token', { path: '/' }); + return null; +} diff --git a/frontend/src/routes/+error.svelte b/frontend/src/routes/+error.svelte new file mode 100644 index 0000000..059e2d4 --- /dev/null +++ b/frontend/src/routes/+error.svelte @@ -0,0 +1,100 @@ + + +
+
+

{page.status === 404 ? 'How did you get here?' : page.status}

+

{page.error?.message ?? 'Something went wrong'}

+ HTTP {page.status} cat + ← Back to safety +
+
+ + diff --git a/frontend/src/routes/+layout.svelte b/frontend/src/routes/+layout.svelte new file mode 100644 index 0000000..484e7e9 --- /dev/null +++ b/frontend/src/routes/+layout.svelte @@ -0,0 +1,70 @@ + + + + Beest + + + +{#if impersonating} + +{/if} + +{@render children()} + + diff --git a/frontend/src/routes/+page.server.ts b/frontend/src/routes/+page.server.ts new file mode 100644 index 0000000..770f97b --- /dev/null +++ b/frontend/src/routes/+page.server.ts @@ -0,0 +1,24 @@ +import { env } from '$env/dynamic/private'; +import type { PageServerLoad } from './$types'; + +const BACKEND_URL = env.BACKEND_URL ?? 'http://localhost:3001'; + +export const load: PageServerLoad = async ({ cookies }) => { + const token = cookies.get('auth_token'); + if (!token) return { authenticated: false }; + + try { + const res = await fetch(`${BACKEND_URL}/api/auth/me`, { + headers: { Authorization: `Bearer ${token}` } + }); + if (res.ok) { + const user = await res.json(); + return { authenticated: true, userName: user.name ?? user.nickname ?? null }; + } + } catch { + // backend unreachable — treat as unauthenticated + } + + cookies.delete('auth_token', { path: '/' }); + return { authenticated: false }; +}; diff --git a/beest/src/routes/+page.svelte b/frontend/src/routes/+page.svelte similarity index 70% rename from beest/src/routes/+page.svelte rename to frontend/src/routes/+page.svelte index 62aface..c596426 100644 --- a/beest/src/routes/+page.svelte +++ b/frontend/src/routes/+page.svelte @@ -15,13 +15,41 @@ - -
- -

Frequently Asked Questions

-

Got questions about Beest? We've got answers. If you need more help, hop into the Beest channel on Hack Club Slack or email euan@hackclub.com

+

I'm sure you have lots of questions! Below is the most common ones I see, but if you need more help please email euan@hackclub.com or use the dedicated slack channel #beest-help

{#each faqs as faq, i (faq.q)}
diff --git a/frontend/src/routes/api/admin/news/+server.ts b/frontend/src/routes/api/admin/news/+server.ts new file mode 100644 index 0000000..8821b9b --- /dev/null +++ b/frontend/src/routes/api/admin/news/+server.ts @@ -0,0 +1,18 @@ +import { env } from '$env/dynamic/private'; +import { proxyWithRefresh } from '$lib/server/auth'; +import type { RequestHandler } from './$types'; + +const BACKEND_URL = env.BACKEND_URL ?? 'http://localhost:3001'; + +export const GET: RequestHandler = async ({ cookies }) => { + return proxyWithRefresh(cookies, `${BACKEND_URL}/api/admin/news`); +}; + +export const POST: RequestHandler = async ({ cookies, request }) => { + const body = await request.json(); + return proxyWithRefresh(cookies, `${BACKEND_URL}/api/admin/news`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body) + }); +}; diff --git a/frontend/src/routes/api/admin/news/[id]/+server.ts b/frontend/src/routes/api/admin/news/[id]/+server.ts new file mode 100644 index 0000000..6b7b6bf --- /dev/null +++ b/frontend/src/routes/api/admin/news/[id]/+server.ts @@ -0,0 +1,20 @@ +import { env } from '$env/dynamic/private'; +import { proxyWithRefresh } from '$lib/server/auth'; +import type { RequestHandler } from './$types'; + +const BACKEND_URL = env.BACKEND_URL ?? 'http://localhost:3001'; + +export const PATCH: RequestHandler = async ({ cookies, params, request }) => { + const body = await request.json(); + return proxyWithRefresh(cookies, `${BACKEND_URL}/api/admin/news/${params.id}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body) + }); +}; + +export const DELETE: RequestHandler = async ({ cookies, params }) => { + return proxyWithRefresh(cookies, `${BACKEND_URL}/api/admin/news/${params.id}`, { + method: 'DELETE' + }); +}; diff --git a/frontend/src/routes/api/admin/orders/+server.ts b/frontend/src/routes/api/admin/orders/+server.ts new file mode 100644 index 0000000..6c8c830 --- /dev/null +++ b/frontend/src/routes/api/admin/orders/+server.ts @@ -0,0 +1,11 @@ +import { env } from '$env/dynamic/private'; +import { proxyWithRefresh } from '$lib/server/auth'; +import type { RequestHandler } from './$types'; + +const BACKEND_URL = env.BACKEND_URL ?? 'http://localhost:3001'; + +export const GET: RequestHandler = async ({ cookies, url }) => { + const params = url.searchParams.toString(); + const qs = params ? `?${params}` : ''; + return proxyWithRefresh(cookies, `${BACKEND_URL}/api/admin/orders${qs}`); +}; diff --git a/frontend/src/routes/api/admin/orders/[id]/fulfill/+server.ts b/frontend/src/routes/api/admin/orders/[id]/fulfill/+server.ts new file mode 100644 index 0000000..7744139 --- /dev/null +++ b/frontend/src/routes/api/admin/orders/[id]/fulfill/+server.ts @@ -0,0 +1,11 @@ +import { env } from '$env/dynamic/private'; +import { proxyWithRefresh } from '$lib/server/auth'; +import type { RequestHandler } from './$types'; + +const BACKEND_URL = env.BACKEND_URL ?? 'http://localhost:3001'; + +export const POST: RequestHandler = async ({ cookies, params }) => { + return proxyWithRefresh(cookies, `${BACKEND_URL}/api/admin/orders/${params.id}/fulfill`, { + method: 'POST' + }); +}; diff --git a/frontend/src/routes/api/admin/orders/[id]/message/+server.ts b/frontend/src/routes/api/admin/orders/[id]/message/+server.ts new file mode 100644 index 0000000..c8658ee --- /dev/null +++ b/frontend/src/routes/api/admin/orders/[id]/message/+server.ts @@ -0,0 +1,14 @@ +import { env } from '$env/dynamic/private'; +import { proxyWithRefresh } from '$lib/server/auth'; +import type { RequestHandler } from './$types'; + +const BACKEND_URL = env.BACKEND_URL ?? 'http://localhost:3001'; + +export const POST: RequestHandler = async ({ cookies, params, request }) => { + const body = await request.json(); + return proxyWithRefresh(cookies, `${BACKEND_URL}/api/admin/orders/${params.id}/message`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body) + }); +}; diff --git a/frontend/src/routes/api/admin/projects/+server.ts b/frontend/src/routes/api/admin/projects/+server.ts new file mode 100644 index 0000000..ab7b828 --- /dev/null +++ b/frontend/src/routes/api/admin/projects/+server.ts @@ -0,0 +1,9 @@ +import { env } from '$env/dynamic/private'; +import { proxyWithRefresh } from '$lib/server/auth'; +import type { RequestHandler } from './$types'; + +const BACKEND_URL = env.BACKEND_URL ?? 'http://localhost:3001'; + +export const GET: RequestHandler = async ({ cookies }) => { + return proxyWithRefresh(cookies, `${BACKEND_URL}/api/admin/projects`); +}; diff --git a/frontend/src/routes/api/admin/projects/[id]/hackatime/+server.ts b/frontend/src/routes/api/admin/projects/[id]/hackatime/+server.ts new file mode 100644 index 0000000..9b0dd2c --- /dev/null +++ b/frontend/src/routes/api/admin/projects/[id]/hackatime/+server.ts @@ -0,0 +1,9 @@ +import { env } from '$env/dynamic/private'; +import { proxyWithRefresh } from '$lib/server/auth'; +import type { RequestHandler } from './$types'; + +const BACKEND_URL = env.BACKEND_URL ?? 'http://localhost:3001'; + +export const GET: RequestHandler = async ({ cookies, params }) => { + return proxyWithRefresh(cookies, `${BACKEND_URL}/api/admin/projects/${params.id}/hackatime`); +}; diff --git a/frontend/src/routes/api/admin/projects/[id]/review/+server.ts b/frontend/src/routes/api/admin/projects/[id]/review/+server.ts new file mode 100644 index 0000000..16db490 --- /dev/null +++ b/frontend/src/routes/api/admin/projects/[id]/review/+server.ts @@ -0,0 +1,14 @@ +import { env } from '$env/dynamic/private'; +import { proxyWithRefresh } from '$lib/server/auth'; +import type { RequestHandler } from './$types'; + +const BACKEND_URL = env.BACKEND_URL ?? 'http://localhost:3001'; + +export const POST: RequestHandler = async ({ cookies, params, request }) => { + const body = await request.text(); + return proxyWithRefresh(cookies, `${BACKEND_URL}/api/admin/projects/${params.id}/review`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body, + }); +}; diff --git a/frontend/src/routes/api/admin/projects/[id]/reviews/+server.ts b/frontend/src/routes/api/admin/projects/[id]/reviews/+server.ts new file mode 100644 index 0000000..28731f2 --- /dev/null +++ b/frontend/src/routes/api/admin/projects/[id]/reviews/+server.ts @@ -0,0 +1,9 @@ +import { env } from '$env/dynamic/private'; +import { proxyWithRefresh } from '$lib/server/auth'; +import type { RequestHandler } from './$types'; + +const BACKEND_URL = env.BACKEND_URL ?? 'http://localhost:3001'; + +export const GET: RequestHandler = async ({ cookies, params }) => { + return proxyWithRefresh(cookies, `${BACKEND_URL}/api/admin/projects/${params.id}/reviews`); +}; diff --git a/frontend/src/routes/api/admin/shop/+server.ts b/frontend/src/routes/api/admin/shop/+server.ts new file mode 100644 index 0000000..9e5cd42 --- /dev/null +++ b/frontend/src/routes/api/admin/shop/+server.ts @@ -0,0 +1,18 @@ +import { env } from '$env/dynamic/private'; +import { proxyWithRefresh } from '$lib/server/auth'; +import type { RequestHandler } from './$types'; + +const BACKEND_URL = env.BACKEND_URL ?? 'http://localhost:3001'; + +export const GET: RequestHandler = async ({ cookies }) => { + return proxyWithRefresh(cookies, `${BACKEND_URL}/api/admin/shop`); +}; + +export const POST: RequestHandler = async ({ cookies, request }) => { + const body = await request.json(); + return proxyWithRefresh(cookies, `${BACKEND_URL}/api/admin/shop`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body) + }); +}; diff --git a/frontend/src/routes/api/admin/shop/[id]/+server.ts b/frontend/src/routes/api/admin/shop/[id]/+server.ts new file mode 100644 index 0000000..ba8bfb9 --- /dev/null +++ b/frontend/src/routes/api/admin/shop/[id]/+server.ts @@ -0,0 +1,20 @@ +import { env } from '$env/dynamic/private'; +import { proxyWithRefresh } from '$lib/server/auth'; +import type { RequestHandler } from './$types'; + +const BACKEND_URL = env.BACKEND_URL ?? 'http://localhost:3001'; + +export const PATCH: RequestHandler = async ({ cookies, params, request }) => { + const body = await request.json(); + return proxyWithRefresh(cookies, `${BACKEND_URL}/api/admin/shop/${params.id}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body) + }); +}; + +export const DELETE: RequestHandler = async ({ cookies, params }) => { + return proxyWithRefresh(cookies, `${BACKEND_URL}/api/admin/shop/${params.id}`, { + method: 'DELETE' + }); +}; diff --git a/frontend/src/routes/api/admin/shop/reorder/+server.ts b/frontend/src/routes/api/admin/shop/reorder/+server.ts new file mode 100644 index 0000000..724698e --- /dev/null +++ b/frontend/src/routes/api/admin/shop/reorder/+server.ts @@ -0,0 +1,14 @@ +import { env } from '$env/dynamic/private'; +import { proxyWithRefresh } from '$lib/server/auth'; +import type { RequestHandler } from './$types'; + +const BACKEND_URL = env.BACKEND_URL ?? 'http://localhost:3001'; + +export const PATCH: RequestHandler = async ({ cookies, request }) => { + const body = await request.json(); + return proxyWithRefresh(cookies, `${BACKEND_URL}/api/admin/shop/reorder`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body) + }); +}; diff --git a/frontend/src/routes/api/admin/users/+server.ts b/frontend/src/routes/api/admin/users/+server.ts new file mode 100644 index 0000000..35a7680 --- /dev/null +++ b/frontend/src/routes/api/admin/users/+server.ts @@ -0,0 +1,9 @@ +import { env } from '$env/dynamic/private'; +import { proxyWithRefresh } from '$lib/server/auth'; +import type { RequestHandler } from './$types'; + +const BACKEND_URL = env.BACKEND_URL ?? 'http://localhost:3001'; + +export const GET: RequestHandler = async ({ cookies }) => { + return proxyWithRefresh(cookies, `${BACKEND_URL}/api/admin/users`); +}; diff --git a/frontend/src/routes/api/admin/users/[id]/+server.ts b/frontend/src/routes/api/admin/users/[id]/+server.ts new file mode 100644 index 0000000..d426012 --- /dev/null +++ b/frontend/src/routes/api/admin/users/[id]/+server.ts @@ -0,0 +1,9 @@ +import { env } from '$env/dynamic/private'; +import { proxyWithRefresh } from '$lib/server/auth'; +import type { RequestHandler } from './$types'; + +const BACKEND_URL = env.BACKEND_URL ?? 'http://localhost:3001'; + +export const GET: RequestHandler = async ({ cookies, params }) => { + return proxyWithRefresh(cookies, `${BACKEND_URL}/api/admin/users/${params.id}`); +}; diff --git a/frontend/src/routes/api/admin/users/[id]/ban/+server.ts b/frontend/src/routes/api/admin/users/[id]/ban/+server.ts new file mode 100644 index 0000000..bc903ea --- /dev/null +++ b/frontend/src/routes/api/admin/users/[id]/ban/+server.ts @@ -0,0 +1,11 @@ +import { env } from '$env/dynamic/private'; +import { proxyWithRefresh } from '$lib/server/auth'; +import type { RequestHandler } from './$types'; + +const BACKEND_URL = env.BACKEND_URL ?? 'http://localhost:3001'; + +export const POST: RequestHandler = async ({ cookies, params }) => { + return proxyWithRefresh(cookies, `${BACKEND_URL}/api/admin/users/${params.id}/ban`, { + method: 'POST' + }); +}; diff --git a/frontend/src/routes/api/admin/users/[id]/impersonate/+server.ts b/frontend/src/routes/api/admin/users/[id]/impersonate/+server.ts new file mode 100644 index 0000000..019f936 --- /dev/null +++ b/frontend/src/routes/api/admin/users/[id]/impersonate/+server.ts @@ -0,0 +1,66 @@ +import { env } from '$env/dynamic/private'; +import { proxyWithRefresh } from '$lib/server/auth'; +import type { RequestHandler } from './$types'; + +const BACKEND_URL = env.BACKEND_URL ?? 'http://localhost:3001'; + +const COOKIE_OPTS = { + path: '/', + httpOnly: true, + sameSite: 'lax' as const, + secure: env.NODE_ENV === 'production' +}; + +/** + * Starts impersonation: saves admin tokens, sets impersonation JWT. + */ +export const POST: RequestHandler = async ({ cookies, params }) => { + // 1. Stash the admin's current tokens BEFORE proxying — proxyWithRefresh + // auto-sets auth_token when the response contains a token field. + const adminToken = cookies.get('auth_token'); + const adminRefresh = cookies.get('refresh_token'); + + // 2. Proxy the impersonate request to backend + const res = await proxyWithRefresh( + cookies, + `${BACKEND_URL}/api/admin/users/${params.id}/impersonate`, + { method: 'POST' } + ); + + if (!res.ok) return res; + + const data = await res.json(); + if (!data.token) { + return new Response(JSON.stringify({ error: 'No token returned' }), { + status: 500, + headers: { 'Content-Type': 'application/json' } + }); + } + + // 3. Save the stashed admin tokens so we can restore them later + if (adminToken) { + cookies.set('admin_auth_token', adminToken, { ...COOKIE_OPTS, maxAge: 3600 }); + } + if (adminRefresh) { + cookies.set('admin_refresh_token', adminRefresh, { ...COOKIE_OPTS, maxAge: 90 * 24 * 60 * 60 }); + } + + // 4. Ensure the impersonation JWT is set (proxyWithRefresh may have already done this) + cookies.set('auth_token', data.token, { ...COOKIE_OPTS, maxAge: 3600 }); + // No refresh token for impersonation — admin must end session before it expires + cookies.delete('refresh_token', { path: '/' }); + + // 4. Set a non-httpOnly cookie so the frontend can detect impersonation + cookies.set('impersonating', '1', { + path: '/', + httpOnly: false, + sameSite: 'lax', + secure: env.NODE_ENV === 'production', + maxAge: 3600 + }); + + return new Response(JSON.stringify({ success: true }), { + status: 200, + headers: { 'Content-Type': 'application/json' } + }); +}; diff --git a/frontend/src/routes/api/admin/users/[id]/perms/+server.ts b/frontend/src/routes/api/admin/users/[id]/perms/+server.ts new file mode 100644 index 0000000..fbe82cd --- /dev/null +++ b/frontend/src/routes/api/admin/users/[id]/perms/+server.ts @@ -0,0 +1,14 @@ +import { env } from '$env/dynamic/private'; +import { proxyWithRefresh } from '$lib/server/auth'; +import type { RequestHandler } from './$types'; + +const BACKEND_URL = env.BACKEND_URL ?? 'http://localhost:3001'; + +export const PATCH: RequestHandler = async ({ cookies, params, request }) => { + const body = await request.json(); + return proxyWithRefresh(cookies, `${BACKEND_URL}/api/admin/users/${params.id}/perms`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body) + }); +}; diff --git a/frontend/src/routes/api/audit-log/+server.ts b/frontend/src/routes/api/audit-log/+server.ts new file mode 100644 index 0000000..0b6953f --- /dev/null +++ b/frontend/src/routes/api/audit-log/+server.ts @@ -0,0 +1,9 @@ +import { env } from '$env/dynamic/private'; +import { proxyWithRefresh } from '$lib/server/auth'; +import type { RequestHandler } from './$types'; + +const BACKEND_URL = env.BACKEND_URL ?? 'http://localhost:3001'; + +export const GET: RequestHandler = async ({ cookies }) => { + return proxyWithRefresh(cookies, `${BACKEND_URL}/api/audit-log`); +}; diff --git a/frontend/src/routes/api/auth/end-impersonate/+server.ts b/frontend/src/routes/api/auth/end-impersonate/+server.ts new file mode 100644 index 0000000..6043864 --- /dev/null +++ b/frontend/src/routes/api/auth/end-impersonate/+server.ts @@ -0,0 +1,42 @@ +import { env } from '$env/dynamic/private'; +import type { RequestHandler } from './$types'; + +const COOKIE_OPTS = { + path: '/', + httpOnly: true, + sameSite: 'lax' as const, + secure: env.NODE_ENV === 'production' +}; + +/** + * Ends impersonation: restores admin's original tokens. + */ +export const POST: RequestHandler = async ({ cookies }) => { + const adminToken = cookies.get('admin_auth_token'); + const adminRefresh = cookies.get('admin_refresh_token'); + + if (!adminToken && !adminRefresh) { + return new Response(JSON.stringify({ error: 'No admin session to restore' }), { + status: 400, + headers: { 'Content-Type': 'application/json' } + }); + } + + // Restore admin tokens + if (adminToken) { + cookies.set('auth_token', adminToken, { ...COOKIE_OPTS, maxAge: 3600 }); + } + if (adminRefresh) { + cookies.set('refresh_token', adminRefresh, { ...COOKIE_OPTS, maxAge: 90 * 24 * 60 * 60 }); + } + + // Clean up impersonation cookies + cookies.delete('admin_auth_token', { path: '/' }); + cookies.delete('admin_refresh_token', { path: '/' }); + cookies.delete('impersonating', { path: '/' }); + + return new Response(JSON.stringify({ success: true }), { + status: 200, + headers: { 'Content-Type': 'application/json' } + }); +}; diff --git a/frontend/src/routes/api/auth/hackatime/start/+server.ts b/frontend/src/routes/api/auth/hackatime/start/+server.ts new file mode 100644 index 0000000..d1e5f35 --- /dev/null +++ b/frontend/src/routes/api/auth/hackatime/start/+server.ts @@ -0,0 +1,53 @@ +import { redirect } from '@sveltejs/kit'; +import { env } from '$env/dynamic/private'; +import { tryRefreshToken } from '$lib/server/auth'; +import type { RequestHandler } from './$types'; + +const BACKEND_URL = env.BACKEND_URL ?? 'http://localhost:3001'; + +// POST only — prevents CSRF via cross-site links (GET + sameSite:lax cookies) +export const POST: RequestHandler = async ({ cookies }) => { + let token = cookies.get('auth_token'); + if (!token) { + token = await tryRefreshToken(cookies) ?? undefined; + if (!token) { + return new Response('Not authenticated', { status: 401 }); + } + } + + const init = { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + } + }; + + let res = await fetch(`${BACKEND_URL}/api/hackatime/start`, init); + + if (res.status === 401) { + const newToken = await tryRefreshToken(cookies); + if (!newToken) { + return new Response('Not authenticated', { status: 401 }); + } + init.headers.Authorization = `Bearer ${newToken}`; + res = await fetch(`${BACKEND_URL}/api/hackatime/start`, init); + } + + if (!res.ok) { + return new Response('Failed to start Hackatime auth', { status: 502 }); + } + + const { url: authorizeUrl, state } = await res.json(); + + // Store state in httpOnly cookie for the callback + cookies.set('hackatime_state', state, { + path: '/', + httpOnly: true, + sameSite: 'lax' as const, + secure: env.NODE_ENV === 'production', + maxAge: 600 + }); + + redirect(302, authorizeUrl); +}; diff --git a/frontend/src/routes/api/auth/login/+server.ts b/frontend/src/routes/api/auth/login/+server.ts new file mode 100644 index 0000000..6e8cb0c --- /dev/null +++ b/frontend/src/routes/api/auth/login/+server.ts @@ -0,0 +1,41 @@ +import { redirect } from '@sveltejs/kit'; +import { env } from '$env/dynamic/private'; +import type { RequestHandler } from './$types'; + +const BACKEND_URL = env.BACKEND_URL ?? 'http://localhost:3001'; + +export const GET: RequestHandler = async ({ url, cookies }) => { + const email = url.searchParams.get('email') ?? undefined; + + // Ask the backend to generate state + authorize URL + let res: Response; + try { + res = await fetch(`${BACKEND_URL}/api/auth/start`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email }) + }); + } catch (err) { + console.error('Failed to reach backend at', BACKEND_URL, err); + return new Response('Backend unreachable', { status: 502 }); + } + + if (!res.ok) { + console.error('Backend returned', res.status, await res.text().catch(() => '')); + return new Response('Failed to start auth', { status: 502 }); + } + + const { url: authorizeUrl, state } = await res.json(); + // Store backend-generated values in httpOnly cookies for the callback + const cookieOpts = { + path: '/', + httpOnly: true, + sameSite: 'lax' as const, + secure: env.NODE_ENV === 'production', + maxAge: 600 + }; + + cookies.set('oauth_state', state, cookieOpts); + + redirect(302, authorizeUrl); +}; diff --git a/frontend/src/routes/api/auth/logout/+server.ts b/frontend/src/routes/api/auth/logout/+server.ts new file mode 100644 index 0000000..c514afb --- /dev/null +++ b/frontend/src/routes/api/auth/logout/+server.ts @@ -0,0 +1,24 @@ +import { redirect } from '@sveltejs/kit'; +import { env } from '$env/dynamic/private'; +import type { RequestHandler } from './$types'; + +const BACKEND_URL = env.BACKEND_URL ?? 'http://localhost:3001'; + +export const GET: RequestHandler = async ({ cookies }) => { + const refreshToken = cookies.get('refresh_token'); + + // Invalidate the session in the DB + if (refreshToken) { + await fetch(`${BACKEND_URL}/api/auth/logout`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ refreshToken }) + }); + } + + // Clear both auth cookies + cookies.delete('auth_token', { path: '/' }); + cookies.delete('refresh_token', { path: '/' }); + + redirect(302, '/'); +}; diff --git a/frontend/src/routes/api/auth/nickname/+server.ts b/frontend/src/routes/api/auth/nickname/+server.ts new file mode 100644 index 0000000..0fafbd8 --- /dev/null +++ b/frontend/src/routes/api/auth/nickname/+server.ts @@ -0,0 +1,14 @@ +import { env } from '$env/dynamic/private'; +import { proxyWithRefresh } from '$lib/server/auth'; +import type { RequestHandler } from './$types'; + +const BACKEND_URL = env.BACKEND_URL ?? 'http://localhost:3001'; + +export const PATCH: RequestHandler = async ({ cookies, request }) => { + const body = await request.text(); + return proxyWithRefresh(cookies, `${BACKEND_URL}/api/auth/nickname`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body + }); +}; diff --git a/frontend/src/routes/api/auth/rsvp/+server.ts b/frontend/src/routes/api/auth/rsvp/+server.ts new file mode 100644 index 0000000..1c1987d --- /dev/null +++ b/frontend/src/routes/api/auth/rsvp/+server.ts @@ -0,0 +1,11 @@ +import { env } from '$env/dynamic/private'; +import { proxyWithRefresh } from '$lib/server/auth'; +import type { RequestHandler } from './$types'; + +const BACKEND_URL = env.BACKEND_URL ?? 'http://localhost:3001'; + +export const POST: RequestHandler = async ({ cookies }) => { + return proxyWithRefresh(cookies, `${BACKEND_URL}/api/auth/rsvp`, { + method: 'POST' + }); +}; diff --git a/frontend/src/routes/api/auth/shipping-eligibility/+server.ts b/frontend/src/routes/api/auth/shipping-eligibility/+server.ts new file mode 100644 index 0000000..019ab7f --- /dev/null +++ b/frontend/src/routes/api/auth/shipping-eligibility/+server.ts @@ -0,0 +1,9 @@ +import { env } from '$env/dynamic/private'; +import { proxyWithRefresh } from '$lib/server/auth'; +import type { RequestHandler } from './$types'; + +const BACKEND_URL = env.BACKEND_URL ?? 'http://localhost:3001'; + +export const GET: RequestHandler = async ({ cookies }) => { + return proxyWithRefresh(cookies, `${BACKEND_URL}/api/auth/shipping-eligibility`); +}; diff --git a/frontend/src/routes/api/cat/+server.ts b/frontend/src/routes/api/cat/+server.ts new file mode 100644 index 0000000..14431a3 --- /dev/null +++ b/frontend/src/routes/api/cat/+server.ts @@ -0,0 +1,22 @@ +import { env } from '$env/dynamic/private'; +import { json } from '@sveltejs/kit'; +import type { RequestHandler } from './$types'; + +const CAT_API_KEY = env.CAT_API_KEY ?? ''; + +export const GET: RequestHandler = async () => { + if (!CAT_API_KEY) { + return json({ url: null }); + } + try { + const res = await fetch( + 'https://api.thecatapi.com/v1/images/search?size=med&mime_types=jpg&format=json&has_breeds=true&order=RANDOM&page=0&limit=1', + { headers: { 'Content-Type': 'application/json', 'x-api-key': CAT_API_KEY } }, + ); + if (!res.ok) return json({ url: null }); + const data = await res.json(); + return json({ url: data?.[0]?.url ?? null }); + } catch { + return json({ url: null }); + } +}; diff --git a/frontend/src/routes/api/hackatime/projects/+server.ts b/frontend/src/routes/api/hackatime/projects/+server.ts new file mode 100644 index 0000000..52f26ae --- /dev/null +++ b/frontend/src/routes/api/hackatime/projects/+server.ts @@ -0,0 +1,9 @@ +import { env } from '$env/dynamic/private'; +import { proxyWithRefresh } from '$lib/server/auth'; +import type { RequestHandler } from './$types'; + +const BACKEND_URL = env.BACKEND_URL ?? 'http://localhost:3001'; + +export const GET: RequestHandler = async ({ cookies }) => { + return proxyWithRefresh(cookies, `${BACKEND_URL}/api/hackatime/projects`); +}; diff --git a/frontend/src/routes/api/leaderboard/+server.ts b/frontend/src/routes/api/leaderboard/+server.ts new file mode 100644 index 0000000..dcc03ac --- /dev/null +++ b/frontend/src/routes/api/leaderboard/+server.ts @@ -0,0 +1,9 @@ +import { env } from '$env/dynamic/private'; +import { proxyWithRefresh } from '$lib/server/auth'; +import type { RequestHandler } from './$types'; + +const BACKEND_URL = env.BACKEND_URL ?? 'http://localhost:3001'; + +export const GET: RequestHandler = async ({ cookies }) => { + return proxyWithRefresh(cookies, `${BACKEND_URL}/api/leaderboard`); +}; diff --git a/frontend/src/routes/api/news/+server.ts b/frontend/src/routes/api/news/+server.ts new file mode 100644 index 0000000..cef9fcd --- /dev/null +++ b/frontend/src/routes/api/news/+server.ts @@ -0,0 +1,9 @@ +import { env } from '$env/dynamic/private'; +import { proxyWithRefresh } from '$lib/server/auth'; +import type { RequestHandler } from './$types'; + +const BACKEND_URL = env.BACKEND_URL ?? 'http://localhost:3001'; + +export const GET: RequestHandler = async ({ cookies }) => { + return proxyWithRefresh(cookies, `${BACKEND_URL}/api/news`); +}; diff --git a/frontend/src/routes/api/onboarding/status/+server.ts b/frontend/src/routes/api/onboarding/status/+server.ts new file mode 100644 index 0000000..68e4f90 --- /dev/null +++ b/frontend/src/routes/api/onboarding/status/+server.ts @@ -0,0 +1,9 @@ +import { env } from '$env/dynamic/private'; +import { proxyWithRefresh } from '$lib/server/auth'; +import type { RequestHandler } from './$types'; + +const BACKEND_URL = env.BACKEND_URL ?? 'http://localhost:3001'; + +export const GET: RequestHandler = async ({ cookies }) => { + return proxyWithRefresh(cookies, `${BACKEND_URL}/api/onboarding/status`); +}; diff --git a/frontend/src/routes/api/onboarding/sticker-link/+server.ts b/frontend/src/routes/api/onboarding/sticker-link/+server.ts new file mode 100644 index 0000000..4909c48 --- /dev/null +++ b/frontend/src/routes/api/onboarding/sticker-link/+server.ts @@ -0,0 +1,9 @@ +import { env } from '$env/dynamic/private'; +import { proxyWithRefresh } from '$lib/server/auth'; +import type { RequestHandler } from './$types'; + +const BACKEND_URL = env.BACKEND_URL ?? 'http://localhost:3001'; + +export const GET: RequestHandler = async ({ cookies }) => { + return proxyWithRefresh(cookies, `${BACKEND_URL}/api/onboarding/sticker-link`); +}; diff --git a/frontend/src/routes/api/onboarding/two-emails/+server.ts b/frontend/src/routes/api/onboarding/two-emails/+server.ts new file mode 100644 index 0000000..ff6a8e2 --- /dev/null +++ b/frontend/src/routes/api/onboarding/two-emails/+server.ts @@ -0,0 +1,11 @@ +import { env } from '$env/dynamic/private'; +import { proxyWithRefresh } from '$lib/server/auth'; +import type { RequestHandler } from './$types'; + +const BACKEND_URL = env.BACKEND_URL ?? 'http://localhost:3001'; + +export const POST: RequestHandler = async ({ cookies }) => { + return proxyWithRefresh(cookies, `${BACKEND_URL}/api/onboarding/two-emails`, { + method: 'POST' + }); +}; diff --git a/frontend/src/routes/api/projects/+server.ts b/frontend/src/routes/api/projects/+server.ts new file mode 100644 index 0000000..ed5e7ac --- /dev/null +++ b/frontend/src/routes/api/projects/+server.ts @@ -0,0 +1,18 @@ +import { env } from '$env/dynamic/private'; +import { proxyWithRefresh } from '$lib/server/auth'; +import type { RequestHandler } from './$types'; + +const BACKEND_URL = env.BACKEND_URL ?? 'http://localhost:3001'; + +export const POST: RequestHandler = async ({ cookies, request }) => { + const body = await request.json(); + return proxyWithRefresh(cookies, `${BACKEND_URL}/api/projects`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body) + }); +}; + +export const GET: RequestHandler = async ({ cookies }) => { + return proxyWithRefresh(cookies, `${BACKEND_URL}/api/projects`); +}; diff --git a/frontend/src/routes/api/projects/[id]/+server.ts b/frontend/src/routes/api/projects/[id]/+server.ts new file mode 100644 index 0000000..e4b3247 --- /dev/null +++ b/frontend/src/routes/api/projects/[id]/+server.ts @@ -0,0 +1,20 @@ +import { env } from '$env/dynamic/private'; +import { proxyWithRefresh } from '$lib/server/auth'; +import type { RequestHandler } from './$types'; + +const BACKEND_URL = env.BACKEND_URL ?? 'http://localhost:3001'; + +export const PATCH: RequestHandler = async ({ cookies, request, params }) => { + const body = await request.json(); + return proxyWithRefresh(cookies, `${BACKEND_URL}/api/projects/${params.id}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body) + }); +}; + +export const DELETE: RequestHandler = async ({ cookies, params }) => { + return proxyWithRefresh(cookies, `${BACKEND_URL}/api/projects/${params.id}`, { + method: 'DELETE' + }); +}; diff --git a/frontend/src/routes/api/projects/[id]/resubmit/+server.ts b/frontend/src/routes/api/projects/[id]/resubmit/+server.ts new file mode 100644 index 0000000..b29a9c4 --- /dev/null +++ b/frontend/src/routes/api/projects/[id]/resubmit/+server.ts @@ -0,0 +1,14 @@ +import { env } from '$env/dynamic/private'; +import { proxyWithRefresh } from '$lib/server/auth'; +import type { RequestHandler } from './$types'; + +const BACKEND_URL = env.BACKEND_URL ?? 'http://localhost:3001'; + +export const POST: RequestHandler = async ({ cookies, request, params }) => { + const body = await request.json(); + return proxyWithRefresh(cookies, `${BACKEND_URL}/api/projects/${params.id}/resubmit`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body) + }); +}; diff --git a/frontend/src/routes/api/projects/explore/+server.ts b/frontend/src/routes/api/projects/explore/+server.ts new file mode 100644 index 0000000..6562b70 --- /dev/null +++ b/frontend/src/routes/api/projects/explore/+server.ts @@ -0,0 +1,9 @@ +import { env } from '$env/dynamic/private'; +import { proxyWithRefresh } from '$lib/server/auth'; +import type { RequestHandler } from './$types'; + +const BACKEND_URL = env.BACKEND_URL ?? 'http://localhost:3001'; + +export const GET: RequestHandler = async ({ cookies }) => { + return proxyWithRefresh(cookies, `${BACKEND_URL}/api/projects/explore`); +}; diff --git a/frontend/src/routes/api/projects/explore/[id]/+server.ts b/frontend/src/routes/api/projects/explore/[id]/+server.ts new file mode 100644 index 0000000..4fd0a87 --- /dev/null +++ b/frontend/src/routes/api/projects/explore/[id]/+server.ts @@ -0,0 +1,9 @@ +import { env } from '$env/dynamic/private'; +import { proxyWithRefresh } from '$lib/server/auth'; +import type { RequestHandler } from './$types'; + +const BACKEND_URL = env.BACKEND_URL ?? 'http://localhost:3001'; + +export const GET: RequestHandler = async ({ cookies, params }) => { + return proxyWithRefresh(cookies, `${BACKEND_URL}/api/projects/explore/${params.id}`); +}; diff --git a/frontend/src/routes/api/projects/explore/[id]/comments/+server.ts b/frontend/src/routes/api/projects/explore/[id]/comments/+server.ts new file mode 100644 index 0000000..24f463d --- /dev/null +++ b/frontend/src/routes/api/projects/explore/[id]/comments/+server.ts @@ -0,0 +1,18 @@ +import { env } from '$env/dynamic/private'; +import { proxyWithRefresh } from '$lib/server/auth'; +import type { RequestHandler } from './$types'; + +const BACKEND_URL = env.BACKEND_URL ?? 'http://localhost:3001'; + +export const GET: RequestHandler = async ({ cookies, params }) => { + return proxyWithRefresh(cookies, `${BACKEND_URL}/api/projects/explore/${params.id}/comments`); +}; + +export const POST: RequestHandler = async ({ cookies, request, params }) => { + const body = await request.json(); + return proxyWithRefresh(cookies, `${BACKEND_URL}/api/projects/explore/${params.id}/comments`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body) + }); +}; diff --git a/frontend/src/routes/api/projects/explore/[id]/comments/[commentId]/+server.ts b/frontend/src/routes/api/projects/explore/[id]/comments/[commentId]/+server.ts new file mode 100644 index 0000000..ecdd66f --- /dev/null +++ b/frontend/src/routes/api/projects/explore/[id]/comments/[commentId]/+server.ts @@ -0,0 +1,13 @@ +import { env } from '$env/dynamic/private'; +import { proxyWithRefresh } from '$lib/server/auth'; +import type { RequestHandler } from './$types'; + +const BACKEND_URL = env.BACKEND_URL ?? 'http://localhost:3001'; + +export const DELETE: RequestHandler = async ({ cookies, params }) => { + return proxyWithRefresh( + cookies, + `${BACKEND_URL}/api/projects/explore/${params.id}/comments/${params.commentId}`, + { method: 'DELETE' } + ); +}; diff --git a/frontend/src/routes/api/projects/hours/+server.ts b/frontend/src/routes/api/projects/hours/+server.ts new file mode 100644 index 0000000..0a38132 --- /dev/null +++ b/frontend/src/routes/api/projects/hours/+server.ts @@ -0,0 +1,9 @@ +import { env } from '$env/dynamic/private'; +import { proxyWithRefresh } from '$lib/server/auth'; +import type { RequestHandler } from './$types'; + +const BACKEND_URL = env.BACKEND_URL ?? 'http://localhost:3001'; + +export const GET: RequestHandler = async ({ cookies }) => { + return proxyWithRefresh(cookies, `${BACKEND_URL}/api/projects/hours`); +}; diff --git a/frontend/src/routes/api/shop/+server.ts b/frontend/src/routes/api/shop/+server.ts new file mode 100644 index 0000000..7863d8b --- /dev/null +++ b/frontend/src/routes/api/shop/+server.ts @@ -0,0 +1,9 @@ +import { env } from '$env/dynamic/private'; +import { proxyWithRefresh } from '$lib/server/auth'; +import type { RequestHandler } from './$types'; + +const BACKEND_URL = env.BACKEND_URL ?? 'http://localhost:3001'; + +export const GET: RequestHandler = async ({ cookies }) => { + return proxyWithRefresh(cookies, `${BACKEND_URL}/api/shop`); +}; diff --git a/frontend/src/routes/api/shop/fulfillment/+server.ts b/frontend/src/routes/api/shop/fulfillment/+server.ts new file mode 100644 index 0000000..5cc5dd0 --- /dev/null +++ b/frontend/src/routes/api/shop/fulfillment/+server.ts @@ -0,0 +1,9 @@ +import { env } from '$env/dynamic/private'; +import { proxyWithRefresh } from '$lib/server/auth'; +import type { RequestHandler } from './$types'; + +const BACKEND_URL = env.BACKEND_URL ?? 'http://localhost:3001'; + +export const GET: RequestHandler = async ({ cookies }) => { + return proxyWithRefresh(cookies, `${BACKEND_URL}/api/shop/fulfillment`); +}; diff --git a/frontend/src/routes/api/shop/fulfillment/read/+server.ts b/frontend/src/routes/api/shop/fulfillment/read/+server.ts new file mode 100644 index 0000000..0455704 --- /dev/null +++ b/frontend/src/routes/api/shop/fulfillment/read/+server.ts @@ -0,0 +1,11 @@ +import { env } from '$env/dynamic/private'; +import { proxyWithRefresh } from '$lib/server/auth'; +import type { RequestHandler } from './$types'; + +const BACKEND_URL = env.BACKEND_URL ?? 'http://localhost:3001'; + +export const POST: RequestHandler = async ({ cookies }) => { + return proxyWithRefresh(cookies, `${BACKEND_URL}/api/shop/fulfillment/read`, { + method: 'POST' + }); +}; diff --git a/frontend/src/routes/api/shop/fulfillment/unread/+server.ts b/frontend/src/routes/api/shop/fulfillment/unread/+server.ts new file mode 100644 index 0000000..933f9e1 --- /dev/null +++ b/frontend/src/routes/api/shop/fulfillment/unread/+server.ts @@ -0,0 +1,9 @@ +import { env } from '$env/dynamic/private'; +import { proxyWithRefresh } from '$lib/server/auth'; +import type { RequestHandler } from './$types'; + +const BACKEND_URL = env.BACKEND_URL ?? 'http://localhost:3001'; + +export const GET: RequestHandler = async ({ cookies }) => { + return proxyWithRefresh(cookies, `${BACKEND_URL}/api/shop/fulfillment/unread`); +}; diff --git a/frontend/src/routes/api/shop/orders/+server.ts b/frontend/src/routes/api/shop/orders/+server.ts new file mode 100644 index 0000000..2d5dfbe --- /dev/null +++ b/frontend/src/routes/api/shop/orders/+server.ts @@ -0,0 +1,9 @@ +import { env } from '$env/dynamic/private'; +import { proxyWithRefresh } from '$lib/server/auth'; +import type { RequestHandler } from './$types'; + +const BACKEND_URL = env.BACKEND_URL ?? 'http://localhost:3001'; + +export const GET: RequestHandler = async ({ cookies }) => { + return proxyWithRefresh(cookies, `${BACKEND_URL}/api/shop/orders`); +}; diff --git a/frontend/src/routes/api/shop/pipes/+server.ts b/frontend/src/routes/api/shop/pipes/+server.ts new file mode 100644 index 0000000..3fb2af5 --- /dev/null +++ b/frontend/src/routes/api/shop/pipes/+server.ts @@ -0,0 +1,9 @@ +import { env } from '$env/dynamic/private'; +import { proxyWithRefresh } from '$lib/server/auth'; +import type { RequestHandler } from './$types'; + +const BACKEND_URL = env.BACKEND_URL ?? 'http://localhost:3001'; + +export const GET: RequestHandler = async ({ cookies }) => { + return proxyWithRefresh(cookies, `${BACKEND_URL}/api/shop/pipes`); +}; diff --git a/frontend/src/routes/api/shop/purchase/+server.ts b/frontend/src/routes/api/shop/purchase/+server.ts new file mode 100644 index 0000000..445d6b2 --- /dev/null +++ b/frontend/src/routes/api/shop/purchase/+server.ts @@ -0,0 +1,14 @@ +import { env } from '$env/dynamic/private'; +import { proxyWithRefresh } from '$lib/server/auth'; +import type { RequestHandler } from './$types'; + +const BACKEND_URL = env.BACKEND_URL ?? 'http://localhost:3001'; + +export const POST: RequestHandler = async ({ cookies, request }) => { + const body = await request.json(); + return proxyWithRefresh(cookies, `${BACKEND_URL}/api/shop/purchase`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body) + }); +}; diff --git a/frontend/src/routes/auth/hackatime/callback/+page.server.ts b/frontend/src/routes/auth/hackatime/callback/+page.server.ts new file mode 100644 index 0000000..0943bc7 --- /dev/null +++ b/frontend/src/routes/auth/hackatime/callback/+page.server.ts @@ -0,0 +1,63 @@ +import { redirect } from '@sveltejs/kit'; +import { env } from '$env/dynamic/private'; +import type { PageServerLoad } from './$types'; + +const BACKEND_URL = env.BACKEND_URL ?? 'http://localhost:3001'; + +export const load: PageServerLoad = async ({ url, cookies }) => { + const code = url.searchParams.get('code'); + const state = url.searchParams.get('state'); + const storedState = cookies.get('hackatime_state'); + const token = cookies.get('auth_token'); + + // Clean up one-time cookie + cookies.delete('hackatime_state', { path: '/' }); + + if (!token) { + redirect(302, '/'); + } + + if (!code || !state) { + const oauthError = + url.searchParams.get('error_description') ?? url.searchParams.get('error'); + if (oauthError) { + console.error(`Hackatime OAuth error: ${oauthError}`); + } + return { error: 'Hackatime connection could not be completed. Please try again.' }; + } + + // Fail early if the state cookie is missing (expired or never set) + if (!storedState) { + return { error: 'Session expired. Please try connecting Hackatime again.' }; + } + + // Forward to backend with both the OAuth params and the user's auth token + const res = await fetch(`${BACKEND_URL}/api/hackatime/callback`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ code, state, storedState }) + }); + + if (!res.ok) { + return { error: 'Hackatime connection failed' }; + } + + const { redirectTo } = await res.json(); + + // Banned users get redirected to fraud page + if (redirectTo === 'https://fraud.hackclub.com/') { + cookies.delete('auth_token', { path: '/' }); + cookies.delete('refresh_token', { path: '/' }); + redirect(302, 'https://fraud.hackclub.com/'); + } + + // Defense-in-depth: only follow relative redirects from the backend + if (typeof redirectTo !== 'string' || !redirectTo.startsWith('/') || redirectTo.startsWith('//')) { + redirect(302, '/tutorial'); + } + + redirect(302, redirectTo); +}; diff --git a/frontend/src/routes/auth/hackatime/callback/+page.svelte b/frontend/src/routes/auth/hackatime/callback/+page.svelte new file mode 100644 index 0000000..b2efa7f --- /dev/null +++ b/frontend/src/routes/auth/hackatime/callback/+page.svelte @@ -0,0 +1,31 @@ + + +{#if data.error} +
+

{data.error}

+ Back to tutorial +
+{:else} +

Connecting Hackatime…

+{/if} + + diff --git a/frontend/src/routes/home/+page.server.ts b/frontend/src/routes/home/+page.server.ts new file mode 100644 index 0000000..1905914 --- /dev/null +++ b/frontend/src/routes/home/+page.server.ts @@ -0,0 +1,28 @@ +import { redirect } from '@sveltejs/kit'; +import { getAuthenticatedUser } from '$lib/server/auth'; +import { env } from '$env/dynamic/private'; +import type { PageServerLoad } from './$types'; + +const BACKEND_URL = env.BACKEND_URL ?? 'http://localhost:3001'; + +export const load: PageServerLoad = async ({ cookies }) => { + const user = await getAuthenticatedUser(cookies); + if (!user) redirect(302, '/'); + + // Check if user has elevated permissions (don't block page load on failure) + let role: string | null = null; + const token = cookies.get('auth_token'); + if (token) { + try { + const res = await fetch(`${BACKEND_URL}/api/auth/scope?scope=reviewer`, { + headers: { Authorization: `Bearer ${token}` } + }); + if (res.ok) { + const data = await res.json(); + role = data.perms ?? null; + } + } catch { /* non-critical */ } + } + + return { user, role }; +}; diff --git a/frontend/src/routes/home/+page.svelte b/frontend/src/routes/home/+page.svelte new file mode 100644 index 0000000..c146606 --- /dev/null +++ b/frontend/src/routes/home/+page.svelte @@ -0,0 +1,5681 @@ + + + + + +
+ + +
+
+ + + + + +

+ #BEEST is built for desktop. For the best experience, please visit on a computer. +

+
+
+ + + + + +
+ + {#if editingProject?.status === 'approved'} + +
+
+ + +
+
+ {#if editingProject.screenshot1Url} + Screenshot + {/if} +
+

{editingProject.name}

+ approved +

{editingProject.description}

+
+ {editingProject.projectType} + {#if editingProject.codeUrl}Code{/if} + {#if editingProject.demoUrl}Demo{/if} + {#if editingProject.readmeUrl}README{/if} +
+
+
+
+ +
+

Resubmit for Review

+

Ship an update to this approved project to earn more Pipes.

+ +
+ + +
+ Be specific about what changed + = 500}>{resubmitChangeDesc.length}/500 +
+ + + + {#if formError} +

{formError}

+ {/if} + + +
+
+
+
+ {:else if creatingProject || editingProject} + +
keystrokes++}> +
+ +
+ +
+
+ + +
+ Give your project a name + = 50}>{projectName.length}/50 +
+
+ +
+ + +
+ Describe your idea + = 300}>{projectDesc.length}/300 +
+
+ +
+
+ + + Link to your source code (GitHub, GitLab, etc) +
+
+ + + Link to your project's README file +
+
+ +
+ + + Link to a live demo or playable version +
+ +
+
+ +
+
+ +
+ + +
+
+
+ {#if screenshotPreviews[activeScreenshot]} + Preview {activeScreenshot + 1} + + {/if} +
+
+ + Upload up to 2 screenshots +
+ +
+ + + + + + +
+ Describe how AI was used in this project + = 200}>{aiUseDescription.length}/200 +
+
+
+ +
+
+ + +
+
+ +
+
+ + +
{ hackatimeOpen = !hackatimeOpen; }}> + {#if hackatimeProject.length === 0} + Select projects + {:else} + {hackatimeProject.join(', ')} + {/if} +
+ {#if hackatimeOpen} +
+ {#if hackatimeLoading} + Loading... + {:else if hackatimeProjects.length === 0} + No projects found + {:else} + {#each hackatimeProjects as proj} + + {/each} + {/if} +
+ {/if} +
+ +
+
+
+ +
+ + +
+ +
+ + {#if formError} +

{formError}

+ {/if} + +
+ {#if otherHcProgram} +
+ + +
+ {/if} + {#if editingProject?.status === 'unreviewed'} +
+

This project is currently in review. You can still work on it and track hours, but you can't resubmit until it's been reviewed.

+ +
+ {/if} +
+ {#if editingProject && editingProject.status !== 'approved'} + + {/if} + + {#if editingProject && editingProject.status !== 'unreviewed'} +
+ + {#if !canSubmitForReview} + Fill out all sections before submitting + {/if} +
+ {/if} +
+
+ + + + +
+ {/if} + + {#if showShippingPrompt && shippingCheck} +
+
+ +

Complete Your Profile

+

Before submitting for review, we need your shipping details so we can send you prizes. Please fill out the missing info in your Hack Club Auth settings:

+
+ {#if !shippingCheck.hasAddress} +
+ + Shipping address not set +
+ {:else} +
+ + Shipping address +
+ {/if} + {#if !shippingCheck.hasBirthdate} +
+ + Birthdate not set +
+ {:else} +
+ + Birthdate +
+ {/if} +
+ + Update on Hack Club Auth + +

After updating your info, log out and log back in to refresh your profile, then try submitting again.

+
+
+ {/if} + + {#if reviewProject} +
+
+ +

Submit "{reviewProject.name}" for Review

+
+ + + + + +
+ {#if formError} +

{formError}

+ {/if} + +
+
+ {/if} + + {#if reviewSubmitted} +
+
+

"{reviewSubmittedName}" Submitted!

+

Review means a human is looking over your project and checking that the code is functional, not AI generated and that the demo works. It could take around a week (hopefully less) for us to get around to your project, at which point we will offer feedback or approve your time spent. In rare cases where we believe you have unintentionally exaggerated your hours, we may approve a percentage of your hours.

+ +
+
+ {/if} + + {#if !creatingProject && !editingProject && !reviewProject && activeSection === 'projects'} +
+
+
+
+

My Projects

+

Track your progress and hours.

+
+
+ Approved + Unreviewed + Changes Needed + Unshipped +
+
+ +
+
+ {displayHours}h + {(hoursByStatus['approved'] ?? 0) >= GOAL_HOURS ? `${GOAL_HOURS}h approved` : `${GOAL_HOURS}h to qualify`} +
+
+ {#each ['approved', 'unreviewed', 'changes_needed', 'unshipped'] as status} + {@const pct = Math.min(((displayByStatus[status] ?? 0) / GOAL_HOURS) * 100, 100)} + {@const label = status === 'changes_needed' ? 'Changes Needed' : status.charAt(0).toUpperCase() + status.slice(1)} + {#if pct > 0} +
+ {/if} + {/each} +
+
+ 0 + 10 + 20 + 30 + 40 +
+
+ +
0} style:--cols={projectCols}> + {#if projects.length === 0} +

No projects yet. Start building to earn hours!

+ + {:else} + {#each projects as project} + {@const isMobile = project.projectType === 'android' || project.projectType === 'ios'} +
openEditProject(project)} onkeydown={(e) => { if (e.key === 'Enter') openEditProject(project); }}> + {#if project.screenshot1Url} + {project.name} screenshot + {:else if catImages[project.id]} +
+ Placeholder cat + placeholder cat - upload your project screenshot instead +
+ {/if} +
+
+

{project.name}

+ {project.projectType} + {project.status === 'changes_needed' ? 'Changes Needed' : project.status} +
+

{project.description}

+ +
+
+ {/each} + {/if} +
+ {#if projects.length > 0} + + {/if} + +
+
+

Action Log

+
+ {#if auditLog.length === 0} +

No activity yet.

+ {:else} + {#each auditLog as entry} +
+ +
+

{entry.label}

+ {timeAgo(entry.createdAt)} +
+
+ {/each} + {/if} +
+
+ +
+

News

+
+ {#if newsItems.length === 0} +

No news yet.

+ {:else} + {#each newsItems as item} +
+ {new Date(item.displayDate + 'T00:00:00').toLocaleDateString('en-US', { month: 'short', day: 'numeric' })} +

{item.text}

+
+ {/each} + {/if} +
+
+
+
+
+ {/if} + + {#if activeSection === 'shop'} +
+
+
+
+
+
+

Earn Prizes

+

Build projects, earn hours, unlock rewards.

+
+
+ Pipes +
+ Pipes + {userPipes} +
+
+
+
+
+
+ {#each {length: 8} as _} + 1 approved hour = 1 pipe, spend pipes on prizes   |    + {/each} +
+
+ {#if shopLoading} +
+ {#each Array(6) as _} +
+
+
+
+
+ {/each} +
+ {:else if shopItems.length === 0} +

No items in the shop yet.

+ {:else} +
+ {#each shopItems as item} + + {/each} +
+ {/if} +
+
+
+ + + {#if selectedShopItem} + +
{ if (e.key === 'Escape') closeShopItem(); }}> + +
e.stopPropagation()}> + +
+
+ {selectedShopItem.name} +
+
+

{selectedShopItem.name}

+

{selectedShopItem.description}

+ +
+ {selectedShopItem.priceHours} Pipes + {#if selectedShopItem.stock !== null} + {selectedShopItem.stock} in stock + {:else} + Unlimited + {/if} +
+ + {#if selectedShopItem.estimatedShip} +
+ + {selectedShopItem.estimatedShip} +
+ {/if} + +
+ Quantity +
+ + {shopQuantity} + +
+
+ +
+ Total: + {selectedShopItem.priceHours * shopQuantity} Pipes +
+ + {#if purchaseSuccess} +
{purchaseSuccess}
+ {:else if userPipes >= selectedShopItem.priceHours * shopQuantity} + + {#if purchaseError} +

{purchaseError}

+ {/if} + {:else} +
+

You need {(selectedShopItem.priceHours * shopQuantity) - userPipes} more Pipes to redeem this.

+

Keep building!

+
+ {/if} +
+
+
+
+ {/if} + {/if} + + {#if activeSection === 'explore'} +
+
+

Explore

+

Discover what others are building, get inspiration!

+ {#if exploreLoading} +
+
+ {#each Array(6) as _} +
+
+
+
+
+ {/each} +
+
+ {:else if exploreProjects.length === 0} +

Awaiting the first projects...

+ {:else} +
+ {#each exploreProjects as ep} + +
openProjectDetail(ep.id)} role="button" tabindex="0" onkeydown={(e) => { if (e.key === 'Enter') openProjectDetail(ep.id); }}> + {#if ep.screenshot1Url || ep.screenshot2Url} +
+ {ep.name} + {#if ep.screenshot1Url && ep.screenshot2Url} + + +
+ + +
+ {/if} +
+ {/if} +
+
+

{ep.name}

+ {ep.projectType} +
+

{ep.description}

+
+ by {ep.builderName} + {#if ep.hours > 0} + {ep.hours.toFixed(1)}h + {/if} +
+
+
+ {/each} +
+ {/if} +
+
+ {/if} + + + {#if detailProject} + + {/if} + + {#if activeSection === 'leaderboard'} +
+
+
+
+

Leaderboard

+

Top builders by approved hours.

+
+
+ Builders + {leaderboardLoading ? '—' : totalBuilders} +
+
+
+
+ # + Builder + Hours +
+ {#if leaderboardLoading} + {#each Array(10) as _, i} +
+ {i + 1} + + +
+ {/each} + {:else if leaderboard.length > 0} + {#each leaderboard as entry, i} +
+ {i + 1} + {entry.name} + {entry.hours}h +
+ {/each} + {/if} +
+
+
+ {/if} + + {#if activeSection === 'faq'} +
+
+ + + + + + +

Frequently Asked Questions

+

I'm sure you have lots of questions! Below is the most common ones I see, but if you need more help please email euan@hackclub.com or use the dedicated slack channel #beest-help

+ +
+ {#each faqItems as faq, i (faq.q)} + + {/each} +
+
+
+ {/if} + + {#if activeSection === 'me'} +
+
+

Me

+ +
+ + +
+ + + + +
+
+
+
+ {/if} + +
+
+ + diff --git a/frontend/src/routes/oauth/callback/+page.server.ts b/frontend/src/routes/oauth/callback/+page.server.ts new file mode 100644 index 0000000..f550d22 --- /dev/null +++ b/frontend/src/routes/oauth/callback/+page.server.ts @@ -0,0 +1,61 @@ +import { redirect } from '@sveltejs/kit'; +import { env } from '$env/dynamic/private'; +import type { PageServerLoad } from './$types'; + +const BACKEND_URL = env.BACKEND_URL ?? 'http://localhost:3001'; + +export const load: PageServerLoad = async ({ url, cookies }) => { + const code = url.searchParams.get('code'); + const state = url.searchParams.get('state'); + const storedState = cookies.get('oauth_state'); + + // Clean up one-time cookie + cookies.delete('oauth_state', { path: '/' }); + + if (!code || !state) { + // Log the provider's error server-side only + const oauthError = url.searchParams.get('error_description') ?? url.searchParams.get('error'); + if (oauthError) { + console.error(`OAuth error from provider: ${oauthError}`); + } + return { error: 'Authentication could not be completed. Please try again.' }; + } + + // Forward everything to the backend + const res = await fetch(`${BACKEND_URL}/api/auth/handle-callback`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ code, state, storedState }) + }); + + if (!res.ok) { + return { error: 'Authentication failed' }; + } + + const { token, refreshToken, redirectTo } = await res.json(); + + const cookieOpts = { + path: '/', + httpOnly: true, + sameSite: 'lax' as const, + secure: env.NODE_ENV === 'production' + }; + + // Banned users get redirected without receiving tokens + if (redirectTo === 'https://fraud.hackclub.com/') { + redirect(302, 'https://fraud.hackclub.com/'); + } + + // Store JWT (1h) and refresh token (90d) in httpOnly cookies + cookies.set('auth_token', token, { ...cookieOpts, maxAge: 3600 }); + cookies.set('refresh_token', refreshToken, { + ...cookieOpts, + maxAge: 90 * 24 * 60 * 60 + }); + + // Defense-in-depth: only follow relative redirects + if (typeof redirectTo !== 'string' || !redirectTo.startsWith('/') || redirectTo.startsWith('//')) { + redirect(302, '/home'); + } + redirect(302, redirectTo); +}; diff --git a/frontend/src/routes/oauth/callback/+page.svelte b/frontend/src/routes/oauth/callback/+page.svelte new file mode 100644 index 0000000..6b6969f --- /dev/null +++ b/frontend/src/routes/oauth/callback/+page.svelte @@ -0,0 +1,34 @@ + + +
+ {#if data.error} +

Login failed: {data.error}

+ Back to home + {:else} +

Redirecting...

+ {/if} +
+ + diff --git a/frontend/src/routes/tutorial/+page.server.ts b/frontend/src/routes/tutorial/+page.server.ts new file mode 100644 index 0000000..c5ce843 --- /dev/null +++ b/frontend/src/routes/tutorial/+page.server.ts @@ -0,0 +1,25 @@ +import { redirect } from '@sveltejs/kit'; +import { env } from '$env/dynamic/private'; +import { getAuthenticatedUser } from '$lib/server/auth'; +import type { PageServerLoad } from './$types'; + +const BACKEND_URL = env.BACKEND_URL ?? 'http://localhost:3001'; + +export const load: PageServerLoad = async ({ url, cookies }) => { + const user = await getAuthenticatedUser(cookies); + if (!user) redirect(302, '/'); + + const token = cookies.get('auth_token')!; + + const onboardingRes = await fetch(`${BACKEND_URL}/api/onboarding/status`, { + headers: { Authorization: `Bearer ${token}` } + }); + + const onboarding = onboardingRes.ok + ? await onboardingRes.json() + : { hackatime: false, slack: false, project: false }; + + const stage = url.searchParams.get('stage'); + + return { user, onboarding, stage: stage ? parseInt(stage, 10) : null }; +}; diff --git a/frontend/src/routes/tutorial/+page.svelte b/frontend/src/routes/tutorial/+page.svelte new file mode 100644 index 0000000..7540328 --- /dev/null +++ b/frontend/src/routes/tutorial/+page.svelte @@ -0,0 +1,1043 @@ + + + + +
+
+
{ if (e.key === 'Enter' || e.key === ' ') skipIntro(); }}> + + + + + + + + + + + + + + + + + + {#if showStart} + + {/if} + + + + + +
+
+ +{#if transitioned} +
+
+
+ {#each clouds as cloud} + + {/each} + + +
+
+

You're all set! If you get stuck you can replay this tutorial, read the FAQ or ask in #beest-help.

+ GO! +
+
+ + +
+

Create a Project

+

Tell us your idea! It doesn't have to be related to the beest, make an automation you've always wanted or a game for you and your friends. Make anything! (Just not AI slop or college projects, we only want to reward creativity and real learning.)

+ {#if projectCreated && !showProjectForm} + + {:else if showProjectForm} +
+ + + + + + + + + + {#if projectError} +

{projectError}

+ {/if} + +
+ + +
+
+ {:else} + + {/if} +
+ + +
+

Connect Hackatime

+

We want to reward you for time spent building, so we made Hackatime! Its like a smart stopwatch that automatically tracks how long you code for, and it works in all your existing code editors. To be rewarded for your work youll need to set up an account on hackatime.hackclub.com and then hit connect to link it to Beest!

+ {#if data.onboarding.hackatime} + + {:else} +
+ +
+ + {/if} +
+ + + +
+

Join Slack

+ {#if slackStatus === 'full_member'} +

It looks like you are already on the Hack Club Slack! We're so glad to have you :)

+ + {:else if slackStatus === 'guest'} +

Hey! It looks like you are new to our community! Check your email for a message from Slack, then follow the instructions in #welcome-to-hack-club. Slack is where everyone is talking - theres 100 THOUSAND technical teens waiting to hear from you.

+ Join! + + + + {:else} +

Join the Hack Club Slack to meet other builders, get help, and share your progress.

+ Join! + + + + {/if} +
+
+ + + +
+
+{#if locked} + +{/if} +{/if} + + diff --git a/frontend/static/favicon.svg b/frontend/static/favicon.svg new file mode 100644 index 0000000..8310a27 --- /dev/null +++ b/frontend/static/favicon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/static/favicon.webp b/frontend/static/favicon.webp new file mode 100644 index 0000000..dd596f9 Binary files /dev/null and b/frontend/static/favicon.webp differ diff --git a/beest/static/fonts/Stone Breaker.otf b/frontend/static/fonts/Stone Breaker.otf similarity index 100% rename from beest/static/fonts/Stone Breaker.otf rename to frontend/static/fonts/Stone Breaker.otf diff --git a/beest/static/fonts/Stone Breaker.woff2 b/frontend/static/fonts/Stone Breaker.woff2 similarity index 100% rename from beest/static/fonts/Stone Breaker.woff2 rename to frontend/static/fonts/Stone Breaker.woff2 diff --git a/beest/static/fonts/SunnyMood.ttf b/frontend/static/fonts/SunnyMood.ttf similarity index 100% rename from beest/static/fonts/SunnyMood.ttf rename to frontend/static/fonts/SunnyMood.ttf diff --git a/beest/static/fonts/SunnyMood.woff2 b/frontend/static/fonts/SunnyMood.woff2 similarity index 100% rename from beest/static/fonts/SunnyMood.woff2 rename to frontend/static/fonts/SunnyMood.woff2 diff --git a/frontend/static/images/Beach.webp b/frontend/static/images/Beach.webp new file mode 100644 index 0000000..fff05d9 Binary files /dev/null and b/frontend/static/images/Beach.webp differ diff --git a/frontend/static/images/Water swooosh.webp b/frontend/static/images/Water swooosh.webp new file mode 100644 index 0000000..57b874c Binary files /dev/null and b/frontend/static/images/Water swooosh.webp differ diff --git a/frontend/static/images/beest-cropped/1.webp b/frontend/static/images/beest-cropped/1.webp new file mode 100644 index 0000000..a10fb7d Binary files /dev/null and b/frontend/static/images/beest-cropped/1.webp differ diff --git a/frontend/static/images/beest-cropped/10.webp b/frontend/static/images/beest-cropped/10.webp new file mode 100644 index 0000000..82f5e0e Binary files /dev/null and b/frontend/static/images/beest-cropped/10.webp differ diff --git a/frontend/static/images/beest-cropped/11.webp b/frontend/static/images/beest-cropped/11.webp new file mode 100644 index 0000000..b8e53a2 Binary files /dev/null and b/frontend/static/images/beest-cropped/11.webp differ diff --git a/frontend/static/images/beest-cropped/2.webp b/frontend/static/images/beest-cropped/2.webp new file mode 100644 index 0000000..cf55a3c Binary files /dev/null and b/frontend/static/images/beest-cropped/2.webp differ diff --git a/frontend/static/images/beest-cropped/3.webp b/frontend/static/images/beest-cropped/3.webp new file mode 100644 index 0000000..9df417b Binary files /dev/null and b/frontend/static/images/beest-cropped/3.webp differ diff --git a/frontend/static/images/beest-cropped/4.webp b/frontend/static/images/beest-cropped/4.webp new file mode 100644 index 0000000..1e9cea0 Binary files /dev/null and b/frontend/static/images/beest-cropped/4.webp differ diff --git a/frontend/static/images/beest-cropped/5.webp b/frontend/static/images/beest-cropped/5.webp new file mode 100644 index 0000000..f7ed526 Binary files /dev/null and b/frontend/static/images/beest-cropped/5.webp differ diff --git a/frontend/static/images/beest-cropped/6.webp b/frontend/static/images/beest-cropped/6.webp new file mode 100644 index 0000000..37f4114 Binary files /dev/null and b/frontend/static/images/beest-cropped/6.webp differ diff --git a/frontend/static/images/beest-cropped/7.webp b/frontend/static/images/beest-cropped/7.webp new file mode 100644 index 0000000..7fa1a18 Binary files /dev/null and b/frontend/static/images/beest-cropped/7.webp differ diff --git a/frontend/static/images/beest-cropped/8.webp b/frontend/static/images/beest-cropped/8.webp new file mode 100644 index 0000000..7584322 Binary files /dev/null and b/frontend/static/images/beest-cropped/8.webp differ diff --git a/frontend/static/images/beest-cropped/9.webp b/frontend/static/images/beest-cropped/9.webp new file mode 100644 index 0000000..3c90e4b Binary files /dev/null and b/frontend/static/images/beest-cropped/9.webp differ diff --git a/beest/static/images/beest.gif b/frontend/static/images/beest.gif similarity index 100% rename from beest/static/images/beest.gif rename to frontend/static/images/beest.gif diff --git a/frontend/static/images/beest.webp b/frontend/static/images/beest.webp new file mode 100644 index 0000000..56801e4 Binary files /dev/null and b/frontend/static/images/beest.webp differ diff --git a/frontend/static/images/beest2.webp b/frontend/static/images/beest2.webp new file mode 100644 index 0000000..eb221a4 Binary files /dev/null and b/frontend/static/images/beest2.webp differ diff --git a/frontend/static/images/bg-tile.webp b/frontend/static/images/bg-tile.webp new file mode 100644 index 0000000..c7310eb Binary files /dev/null and b/frontend/static/images/bg-tile.webp differ diff --git a/frontend/static/images/cloud-left.webp b/frontend/static/images/cloud-left.webp new file mode 100644 index 0000000..dbbf3df Binary files /dev/null and b/frontend/static/images/cloud-left.webp differ diff --git a/frontend/static/images/cloud-right.webp b/frontend/static/images/cloud-right.webp new file mode 100644 index 0000000..5e99869 Binary files /dev/null and b/frontend/static/images/cloud-right.webp differ diff --git a/frontend/static/images/cloud.webp b/frontend/static/images/cloud.webp new file mode 100644 index 0000000..01626c2 Binary files /dev/null and b/frontend/static/images/cloud.webp differ diff --git a/beest/static/images/frames/75 teens at Campfire Flagship.webp b/frontend/static/images/frames/75 teens at Campfire Flagship.webp similarity index 100% rename from beest/static/images/frames/75 teens at Campfire Flagship.webp rename to frontend/static/images/frames/75 teens at Campfire Flagship.webp diff --git a/beest/static/images/frames/Hackathon on an island.webp b/frontend/static/images/frames/Hackathon on an island.webp similarity index 100% rename from beest/static/images/frames/Hackathon on an island.webp rename to frontend/static/images/frames/Hackathon on an island.webp diff --git a/beest/static/images/frames/Teen hackers at Assemble.webp b/frontend/static/images/frames/Teen hackers at Assemble.webp similarity index 100% rename from beest/static/images/frames/Teen hackers at Assemble.webp rename to frontend/static/images/frames/Teen hackers at Assemble.webp diff --git a/beest/static/images/frames/Teens at a local game Jam.webp b/frontend/static/images/frames/Teens at a local game Jam.webp similarity index 100% rename from beest/static/images/frames/Teens at a local game Jam.webp rename to frontend/static/images/frames/Teens at a local game Jam.webp diff --git a/beest/static/images/frames/Winners of Parthenon Hackathon.webp b/frontend/static/images/frames/Winners of Parthenon Hackathon.webp similarity index 100% rename from beest/static/images/frames/Winners of Parthenon Hackathon.webp rename to frontend/static/images/frames/Winners of Parthenon Hackathon.webp diff --git a/beest/static/images/frames/hackers debugging together.webp b/frontend/static/images/frames/hackers debugging together.webp similarity index 100% rename from beest/static/images/frames/hackers debugging together.webp rename to frontend/static/images/frames/hackers debugging together.webp diff --git a/frontend/static/images/hero-1024w.webp b/frontend/static/images/hero-1024w.webp new file mode 100644 index 0000000..2aea1ff Binary files /dev/null and b/frontend/static/images/hero-1024w.webp differ diff --git a/frontend/static/images/hero-1440w.webp b/frontend/static/images/hero-1440w.webp new file mode 100644 index 0000000..2c0cd96 Binary files /dev/null and b/frontend/static/images/hero-1440w.webp differ diff --git a/frontend/static/images/hero-640w.webp b/frontend/static/images/hero-640w.webp new file mode 100644 index 0000000..89f1155 Binary files /dev/null and b/frontend/static/images/hero-640w.webp differ diff --git a/beest/static/images/hero.webp b/frontend/static/images/hero.webp similarity index 100% rename from beest/static/images/hero.webp rename to frontend/static/images/hero.webp diff --git a/frontend/static/images/pipes.png b/frontend/static/images/pipes.png new file mode 100644 index 0000000..8df908b Binary files /dev/null and b/frontend/static/images/pipes.png differ diff --git a/beest/static/images/shop/blahaj.webp b/frontend/static/images/shop/blahaj.webp similarity index 100% rename from beest/static/images/shop/blahaj.webp rename to frontend/static/images/shop/blahaj.webp diff --git a/beest/static/images/shop/flight-stipend.webp b/frontend/static/images/shop/flight-stipend.webp similarity index 100% rename from beest/static/images/shop/flight-stipend.webp rename to frontend/static/images/shop/flight-stipend.webp diff --git a/beest/static/images/shop/framework.webp b/frontend/static/images/shop/framework.webp similarity index 100% rename from beest/static/images/shop/framework.webp rename to frontend/static/images/shop/framework.webp diff --git a/beest/static/images/shop/headphones.webp b/frontend/static/images/shop/headphones.webp similarity index 100% rename from beest/static/images/shop/headphones.webp rename to frontend/static/images/shop/headphones.webp diff --git a/beest/static/images/shop/polaroid.webp b/frontend/static/images/shop/polaroid.webp similarity index 100% rename from beest/static/images/shop/polaroid.webp rename to frontend/static/images/shop/polaroid.webp diff --git a/beest/static/images/shop/poster.webp b/frontend/static/images/shop/poster.webp similarity index 100% rename from beest/static/images/shop/poster.webp rename to frontend/static/images/shop/poster.webp diff --git a/beest/static/images/shop/printer.webp b/frontend/static/images/shop/printer.webp similarity index 100% rename from beest/static/images/shop/printer.webp rename to frontend/static/images/shop/printer.webp diff --git a/beest/static/images/shop/stickers.webp b/frontend/static/images/shop/stickers.webp similarity index 100% rename from beest/static/images/shop/stickers.webp rename to frontend/static/images/shop/stickers.webp diff --git a/frontend/static/images/sky.webp b/frontend/static/images/sky.webp new file mode 100644 index 0000000..703eded Binary files /dev/null and b/frontend/static/images/sky.webp differ diff --git a/frontend/static/images/sticker.jpg b/frontend/static/images/sticker.jpg new file mode 100644 index 0000000..76a73a8 Binary files /dev/null and b/frontend/static/images/sticker.jpg differ diff --git a/frontend/static/images/sticker.webp b/frontend/static/images/sticker.webp new file mode 100644 index 0000000..8a65086 Binary files /dev/null and b/frontend/static/images/sticker.webp differ diff --git a/frontend/static/images/tile.webp b/frontend/static/images/tile.webp new file mode 100644 index 0000000..bca959d Binary files /dev/null and b/frontend/static/images/tile.webp differ diff --git a/frontend/static/images/tile2.webp b/frontend/static/images/tile2.webp new file mode 100644 index 0000000..a8a3abe Binary files /dev/null and b/frontend/static/images/tile2.webp differ diff --git a/frontend/static/images/tile3.webp b/frontend/static/images/tile3.webp new file mode 100644 index 0000000..5139afd Binary files /dev/null and b/frontend/static/images/tile3.webp differ diff --git a/frontend/static/images/tutorial-top.webp b/frontend/static/images/tutorial-top.webp new file mode 100644 index 0000000..36a714f Binary files /dev/null and b/frontend/static/images/tutorial-top.webp differ diff --git a/beest/static/robots.txt b/frontend/static/robots.txt similarity index 100% rename from beest/static/robots.txt rename to frontend/static/robots.txt diff --git a/beest/svelte.config.js b/frontend/svelte.config.js similarity index 100% rename from beest/svelte.config.js rename to frontend/svelte.config.js diff --git a/beest/tsconfig.json b/frontend/tsconfig.json similarity index 100% rename from beest/tsconfig.json rename to frontend/tsconfig.json diff --git a/beest/vite.config.ts b/frontend/vite.config.ts similarity index 100% rename from beest/vite.config.ts rename to frontend/vite.config.ts