diff --git a/scouthub-magazzino-be/.env.example b/scouthub-magazzino-be/.env.example new file mode 100644 index 0000000..45bab7a --- /dev/null +++ b/scouthub-magazzino-be/.env.example @@ -0,0 +1,7 @@ +PORT=8002 +DATABASE_URL=postgresql://postgres:postgres@localhost:5432/scouthub_magazzino?schema=public + +KEYCLOAK_BASE_URL=http://localhost:6999 +KEYCLOAK_REALM=scouthub +KEYCLOAK_MAGAZZINO_CLIENT_ID=scouthub-magazzino-be +KEYCLOAK_MAGAZZINO_CLIENT_SECRET=CAMBIA-QUESTO-SECRET-IN-UN-VAULT diff --git a/scouthub-magazzino-be/.gitignore b/scouthub-magazzino-be/.gitignore new file mode 100644 index 0000000..30d235a --- /dev/null +++ b/scouthub-magazzino-be/.gitignore @@ -0,0 +1,8 @@ +node_modules/ +dist/ +.env +*.log +coverage/ + +/magazzino-be.iml +/.idea/ diff --git a/scouthub-magazzino-be/Dockerfile b/scouthub-magazzino-be/Dockerfile new file mode 100644 index 0000000..471fbad --- /dev/null +++ b/scouthub-magazzino-be/Dockerfile @@ -0,0 +1,16 @@ +FROM node:20-bookworm-slim + +RUN apt-get update && apt-get install -y openssl && rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +COPY package*.json ./ +RUN npm ci + +COPY . . +RUN npx prisma generate +RUN npm run build + +EXPOSE 8083 + +CMD ["sh", "-c", "npx prisma migrate deploy && node dist/server.js"] diff --git a/scouthub-magazzino-be/README.md b/scouthub-magazzino-be/README.md new file mode 100644 index 0000000..7442a12 --- /dev/null +++ b/scouthub-magazzino-be/README.md @@ -0,0 +1,89 @@ +README.# scouthub-magazzino-be + +Backend Node.js/TypeScript per la gestione del **magazzino** Scouthub (materiale scout in dotazione +ai gruppi), pensato per affiancare `scouthub-home-be` e `scouthub-attivita-be` nello stesso ecosistema. + +> Stato attuale: schema dati e middleware di autenticazione pronti; espone solo l'healthcheck, +> nessuna route di dominio (liste/magazzino/eventi) ancora implementata. + +## Stack + +- Node.js ≥ 20, TypeScript +- Express +- Prisma ORM su PostgreSQL (database dedicato `scouthub_magazzino`) +- Autenticazione JWT via Keycloak, stesso realm di `scouthub-home-be`/`scouthub-attivita-be` + +## Struttura a livelli + +``` +src/ + routes/ # definizione degli endpoint Express + controllers/ # gestione request/response, delega alla service layer + services/ # business logic + repositories/ # accesso ai dati tramite Prisma + auth/ # verifica JWT Keycloak e guard sui ruoli/org_id + config/ # lettura e validazione delle variabili d'ambiente + db/ # istanza condivisa di PrismaClient + middleware/ # error handler +``` + +Flusso delle richieste: `routes -> controller -> service -> repository (Prisma)`. + +## Autenticazione e multi-tenancy (org_id) + +- `src/auth/verify-token.middleware.ts` valida il Bearer token contro il JWKS del realm Keycloak + (`${KEYCLOAK_BASE_URL}/realms/${KEYCLOAK_REALM}/protocol/openid-connect/certs`) e popola + `req.auth` con `userId`, `email`, `orgId` (dal claim `organization` iniettato da Keycloak + Organizations) e `roles` (ruoli realm + ruoli sull'organizzazione attiva). +- `src/auth/require-org-id.middleware.ts` da usare dopo `verifyToken` su **tutte** le route + private (liste, magazzino, eventi): rifiuta la richiesta se il token non porta + un'organizzazione attiva. Le query verso il database devono sempre filtrare per + `req.auth.orgId`, mai per un `org_id` letto da params/query/body della richiesta. +- `src/auth/require-moderatore.middleware.ts` da usare solo sulle route di moderazione del + catalogo materiali (richiede il ruolo realm `moderatore`). + +## Variabili d'ambiente + +Vedi `.env.example`. Copiarlo in `.env` e valorizzare: + +| Variabile | Descrizione | +|---|---| +| `PORT` | Porta HTTP del servizio (default `8083`) | +| `DATABASE_URL` | Connection string Postgres (schema/database `scouthub_magazzino`) | +| `KEYCLOAK_BASE_URL` | Base URL del server Keycloak | +| `KEYCLOAK_REALM` | Realm Keycloak (`scouthub`) | +| `KEYCLOAK_MAGAZZINO_CLIENT_ID` | Client Keycloak dedicato a questo servizio | +| `KEYCLOAK_MAGAZZINO_CLIENT_SECRET` | Secret del client sopra | + +## Avvio in locale + +```bash +npm install +npx prisma generate +npx prisma migrate deploy # applica le migration sul database scouthub_magazzino +npm run dev # avvia con ts-node-dev su http://localhost:8083 +``` + +## Build e avvio in produzione + +```bash +npm run build +npm start +``` + +## Healthcheck + +``` +GET /health +``` + +Risponde `{ "status": "ok", "database": "up" }` se il servizio e la connessione al database sono +funzionanti. + +## Test + +```bash +npm test +``` + +Nessun test presente al momento (scaffold); il comando gira con `--passWithNoTests`. diff --git a/scouthub-magazzino-be/jest.config.ts b/scouthub-magazzino-be/jest.config.ts new file mode 100644 index 0000000..b526004 --- /dev/null +++ b/scouthub-magazzino-be/jest.config.ts @@ -0,0 +1,14 @@ +import type { Config } from 'jest'; + +const config: Config = { + preset: 'ts-jest', + testEnvironment: 'node', + rootDir: '.', + testMatch: ['/tests/unit/**/*.test.ts', '/tests/integration/**/*.test.ts'], + testTimeout: 15000, + transform: { + '^.+\\.ts$': ['ts-jest', { tsconfig: 'tsconfig.jest.json' }], + }, +}; + +export default config; diff --git a/scouthub-magazzino-be/package-lock.json b/scouthub-magazzino-be/package-lock.json new file mode 100644 index 0000000..df85b8e --- /dev/null +++ b/scouthub-magazzino-be/package-lock.json @@ -0,0 +1,5908 @@ +{ + "name": "scouthub-magazzino-be", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "scouthub-magazzino-be", + "version": "1.0.0", + "dependencies": { + "@prisma/client": "^5.16.1", + "cors": "^2.8.5", + "dotenv": "^16.4.5", + "express": "^4.19.2", + "jsonwebtoken": "^9.0.3", + "jwks-rsa": "^3.2.2" + }, + "devDependencies": { + "@types/cors": "^2.8.17", + "@types/express": "^4.17.21", + "@types/jest": "^29.5.12", + "@types/jsonwebtoken": "^9.0.10", + "@types/node": "^20.14.12", + "@types/supertest": "^7.2.1", + "jest": "^29.7.0", + "nock": "^13.5.4", + "prisma": "^5.16.1", + "supertest": "^7.2.2", + "ts-jest": "^29.2.3", + "ts-node": "^10.9.2", + "ts-node-dev": "^2.0.0", + "typescript": "^5.5.4" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@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/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@babel/core/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/generator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "@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.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "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.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.29.7.tgz", + "integrity": "sha512-zGYcYfq/WmZ4V+kBIXQon9dSSc8ircGZqw9ZaNhhGj9nZkeBu1jHLBDQqYYi5WA9uawvA2sIMbry2nCFhf5Djg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "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.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.29.7.tgz", + "integrity": "sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "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.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.29.7.tgz", + "integrity": "sha512-ngr+82Sh0xMz25TPCZi+nC2iTzjfCdWS2ONXTp/PtSCHCgaCNBpdMqgvJ2ccdLlClVZ7sisIgB914j/JFe+RZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@babel/traverse/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "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/@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==", + "dev": 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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "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/schema": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.6.tgz", + "integrity": "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "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==", + "dev": 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/@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/@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/@prisma/client": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.22.0.tgz", + "integrity": "sha512-M0SVXfyHnQREBKxCgyo7sffrKttwE6R8PMq330MIUF0pTwjUhLbW84pFDlf06B27XyCR++VtjugEnIHdr07SVA==", + "hasInstallScript": true, + "license": "Apache-2.0", + "engines": { + "node": ">=16.13" + }, + "peerDependencies": { + "prisma": "*" + }, + "peerDependenciesMeta": { + "prisma": { + "optional": true + } + } + }, + "node_modules/@prisma/debug": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.22.0.tgz", + "integrity": "sha512-AUt44v3YJeggO2ZU5BkXI7M4hu9BF2zzH2iF2V5pyXT/lRTyWiElZ7It+bRH1EshoMRxHgpYg4VB6rCM+mG5jQ==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/engines": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.22.0.tgz", + "integrity": "sha512-UNjfslWhAt06kVL3CjkuYpHAWSO6L4kDCVPegV6itt7nD1kSJavd3vhgAEhjglLJJKEdJ7oIqDJ+yHk6qO8gPA==", + "devOptional": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "5.22.0", + "@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", + "@prisma/fetch-engine": "5.22.0", + "@prisma/get-platform": "5.22.0" + } + }, + "node_modules/@prisma/engines-version": { + "version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2.tgz", + "integrity": "sha512-2PTmxFR2yHW/eB3uqWtcgRcgAbG1rwG9ZriSvQw+nnb7c4uCr3RAcGMb6/zfE88SKlC1Nj2ziUvc96Z379mHgQ==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/fetch-engine": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.22.0.tgz", + "integrity": "sha512-bkrD/Mc2fSvkQBV5EpoFcZ87AvOgDxbG99488a5cexp5Ccny+UM6MAe/UFkUC0wLYD9+9befNOqGiIJhhq+HbA==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "5.22.0", + "@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", + "@prisma/get-platform": "5.22.0" + } + }, + "node_modules/@prisma/get-platform": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.22.0.tgz", + "integrity": "sha512-pHhpQdr1UPFpt+zFfnPazhulaZYCUqeIcPpJViYoq9R+D/yw4fjE+CtnsnKzPYm0ddUbeXUzjGVGIRVgPDCk4Q==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "5.22.0" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.12", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.12.tgz", + "integrity": "sha512-hhyNJ+nbR6ZR7pToHvllEFun9TL0sbL+tk/ON75lo+Xas054uez98qRbsuNt7MBCyZKK4+8Yli/OAGZhmfBZ/g==", + "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": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "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==", + "dev": 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==", + "dev": 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==", + "dev": 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==", + "dev": true, + "license": "MIT" + }, + "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/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/express": { + "version": "4.17.25", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz", + "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "^1" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.19.9", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.9.tgz", + "integrity": "sha512-QP2ESEe/ImWY0HDwNAnK9PvEffUyhLTnWkk7KXzHfyeWAnlrDe1fN77bXl6ia8KT3wPlmA7t9/VPRpnf4Ex9sg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "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": "29.5.14", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.14.tgz", + "integrity": "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, + "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/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "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": "20.19.43", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz", + "integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/qs": { + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.10", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz", + "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "<1" + } + }, + "node_modules/@types/serve-static/node_modules/@types/send": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz", + "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@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/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-xevGOReSYGM7g/kUBZzPqCrR/KYAo+F0yiPc85WFTJa0MSLtyFTVTU6cJu/aV4mid7IffDIWqo69THF2o4JiEQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/strip-json-comments": { + "version": "0.0.30", + "resolved": "https://registry.npmjs.org/@types/strip-json-comments/-/strip-json-comments-0.0.30.tgz", + "integrity": "sha512-7NQmHra/JILCd1QqpSzl8+mJRc8ZHz3uDm8YV1Ks9IhK0epEiTw8aIErbvH9PI+6XbqhyIQy3462nEsn7UVzjQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/superagent": { + "version": "8.1.11", + "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-8.1.11.tgz", + "integrity": "sha512-KA7srSW/HENDtOw9DOqaFLgWuMqN9WgjEw62lh9dpvRaZDkhdOkazASd7X7i2eMUYLHa1U37ZttnePsH5zTDHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/cookiejar": "^2.1.5", + "@types/methods": "^1.1.4", + "@types/node": "*", + "form-data": "^4.0.0" + } + }, + "node_modules/@types/supertest": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/@types/supertest/-/supertest-7.2.1.tgz", + "integrity": "sha512-4CbBvoYVLHL7+yhbYrZET0vsvuyXTC05aRe7dNQkwMzm56auceoy6Yu3K50uZmwfHna1os3CMSgM/3QVkUtPTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/methods": "^1.1.4", + "@types/superagent": "^8.1.0" + } + }, + "node_modules/@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/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.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==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "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/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "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/babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.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": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.1.tgz", + "integrity": "sha512-HYXq73DDpCtNzOmrFsm9eSwCvWCql0RzqjpDzXN9EadiLJ4DNat0nsZ/Bzmy+Ud12mb4/zKDY0cQ805ZzN+i0A==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/body-parser": { + "version": "1.20.6", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.6.tgz", + "integrity": "sha512-p5tAzS57i5MV9fZFDj9LeIiTZEufbSe2eDozP+ElheSUq1m74CRq1jI4mYNDdVs9vQztXFLuk/Gd6BWTdwRJ5g==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.15.1", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz", + "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", + "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.7", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.7.tgz", + "integrity": "sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw==", + "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.10.44", + "caniuse-lite": "^1.0.30001806", + "electron-to-chromium": "^1.5.393", + "node-releases": "^2.0.51", + "update-browserslist-db": "^1.2.3" + }, + "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-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==", + "dev": true, + "license": "MIT" + }, + "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-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.30001806", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001806.tgz", + "integrity": "sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==", + "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/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", + "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/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==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/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/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/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "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.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/cookiejar": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", + "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", + "dev": true, + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "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==", + "dev": 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==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/dedent": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.2.tgz", + "integrity": "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "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/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/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "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==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "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/dynamic-dedupe": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/dynamic-dedupe/-/dynamic-dedupe-0.3.0.tgz", + "integrity": "sha512-ssuANeD+z97meYOqd50e04Ze5qp4bPqo8cCkI4TRjZkzAUgIDTrXV1R8QCdINpiI+hw14+rYazvTRdQrz0/rFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + } + }, + "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.396", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.396.tgz", + "integrity": "sha512-yHiw2Y3C3H9U6TMbOfoWK/BPreiOPXRfTWPBwQBoZG6/8TB6eOPnsy5oaRYuatR7Fw2SJ4kKforgufeo7fq0EQ==", + "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==", + "dev": true, + "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/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-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/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": "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/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/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/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/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/express": { + "version": "4.22.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz", + "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.5", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.15.1", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "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-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==", + "dev": true, + "license": "MIT" + }, + "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/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": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "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/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/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": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "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==", + "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==", + "dev": true, + "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": "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/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "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.9", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.9.tgz", + "integrity": "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==", + "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/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-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "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-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.3" + }, + "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==", + "dev": true, + "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-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-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/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/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-instrument/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/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": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/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/jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.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": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.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": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.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": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.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/jose": { + "version": "4.15.9", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz", + "integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "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": "3.15.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.0.tgz", + "integrity": "sha512-ttBQIIQPDeLjpPOohtUdXuXUVoA2uIB6fEH9HyJ7234s5mBJ5wTx20njxplLZQgLaOfpmPQA7X2t5AX6tIPbog==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "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-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-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "dev": true, + "license": "ISC" + }, + "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/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/jsonwebtoken/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/jsonwebtoken/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "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/jwks-rsa": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-3.2.2.tgz", + "integrity": "sha512-BqTyEDV+lS8F2trk3A+qJnxV5Q9EqKCBJOPti3W97r7qTympCZjb7h2X6f2kc+0K3rsSTY1/6YG2eaXKoj497w==", + "license": "MIT", + "dependencies": { + "@types/jsonwebtoken": "^9.0.4", + "debug": "^4.3.4", + "jose": "^4.15.4", + "limiter": "^1.1.5", + "lru-memoizer": "^2.2.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/jwks-rsa/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/jwks-rsa/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/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/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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/limiter": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz", + "integrity": "sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA==" + }, + "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/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/lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==", + "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.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/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/lru-memoizer": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/lru-memoizer/-/lru-memoizer-2.3.0.tgz", + "integrity": "sha512-GXn7gyHAMhO13WSKrIiNfztwxodVsP8IoZ3XfrJV4yH2x0/OeTO/FIaAHTY5YekdGgW94njfuKmyyt1E0mR6Ug==", + "license": "MIT", + "dependencies": { + "lodash.clonedeep": "^4.5.0", + "lru-cache": "6.0.0" + } + }, + "node_modules/lru-memoizer/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/lru-memoizer/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, + "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-dir/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": 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": "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/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "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==", + "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/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "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/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/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/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "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/nock": { + "version": "13.5.6", + "resolved": "https://registry.npmjs.org/nock/-/nock-13.5.6.tgz", + "integrity": "sha512-o2zOYiCpzRqSzPj0Zt/dQ/DqZeYoaQ7TUonc/xUPjCGl9WeHpNbxgVvOquXYAaJzI0M9BXV3HTzG0p8IUAbBTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.0", + "json-stringify-safe": "^5.0.1", + "propagate": "^2.0.0" + }, + "engines": { + "node": ">= 10.13" + } + }, + "node_modules/nock/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/nock/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/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.51", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.51.tgz", + "integrity": "sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "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==", + "dev": true, + "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/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": "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/p-locate/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/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/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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-to-regexp": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", + "license": "MIT" + }, + "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": "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/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/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.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/prisma": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.22.0.tgz", + "integrity": "sha512-vtpjW3XuYCSnMsNVBjLMNkTj6OZbudcPPTPYHqX0CJfpcdWciI1dM8uHETwmDxxiqEwCIE6WvXucWUetJgfu/A==", + "devOptional": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/engines": "5.22.0" + }, + "bin": { + "prisma": "build/index.js" + }, + "engines": { + "node": ">=16.13" + }, + "optionalDependencies": { + "fsevents": "2.3.3" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/propagate": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/propagate/-/propagate-2.0.1.tgz", + "integrity": "sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "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/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "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.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", + "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", + "license": "BSD-3-Clause", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/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": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "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/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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-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.exports": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", + "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/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/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/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/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/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "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.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "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": "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/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "license": "MIT" + }, + "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.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/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/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/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/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/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==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "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/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/superagent/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/superagent/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/superagent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/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/supertest/node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.6.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/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/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-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/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/ts-jest": { + "version": "29.4.12", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.12.tgz", + "integrity": "sha512-Ov6ClY53Fflh6BGAnY2DlTq1hYDrTycz2PVTXBWFW2CU+9zrEqAp9fWdGXl42EXO5RLSFAcAZ2JFKbP+zBTFfw==", + "dev": true, + "license": "MIT", + "dependencies": { + "bs-logger": "^0.2.6", + "fast-json-stable-stringify": "^2.1.0", + "handlebars": "^4.7.9", + "json5": "^2.2.3", + "lodash.memoize": "^4.1.2", + "make-error": "^1.3.6", + "semver": "^7.8.5", + "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 <7" + }, + "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/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/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-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": 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/ts-node-dev": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ts-node-dev/-/ts-node-dev-2.0.0.tgz", + "integrity": "sha512-ywMrhCfH6M75yftYvrvNarLEY+SUXtUvU8/0Z6llrHQVBx12GiFk5sStF8UdfE/yfzk9IAq7O5EEbTQsxlBI8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.1", + "dynamic-dedupe": "^0.3.0", + "minimist": "^1.2.6", + "mkdirp": "^1.0.4", + "resolve": "^1.0.0", + "rimraf": "^2.6.1", + "source-map-support": "^0.5.12", + "tree-kill": "^1.2.2", + "ts-node": "^10.4.0", + "tsconfig": "^7.0.0" + }, + "bin": { + "ts-node-dev": "lib/bin.js", + "tsnd": "lib/bin.js" + }, + "engines": { + "node": ">=0.8.0" + }, + "peerDependencies": { + "node-notifier": "*", + "typescript": "*" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/tsconfig": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/tsconfig/-/tsconfig-7.0.0.tgz", + "integrity": "sha512-vZXmzPrL+EmC4T/4rVlT2jNVMWCi/O4DIiSj3UHg1OE5kCKbk4mfrXc6dZksLgRM/TZlKnousKH9bbTazUWRRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/strip-bom": "^3.0.0", + "@types/strip-json-comments": "0.0.30", + "strip-bom": "^3.0.0", + "strip-json-comments": "^2.0.0" + } + }, + "node_modules/tsconfig/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/tsconfig/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.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": "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/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "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/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/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/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/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "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==", + "dev": 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/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/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": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.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==", + "dev": true, + "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==", + "dev": true, + "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.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": 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" + } + } + } +} diff --git a/scouthub-magazzino-be/package.json b/scouthub-magazzino-be/package.json new file mode 100644 index 0000000..118b4de --- /dev/null +++ b/scouthub-magazzino-be/package.json @@ -0,0 +1,42 @@ +{ + "name": "scouthub-magazzino-be", + "version": "1.0.0", + "private": true, + "description": "Backend Node.js/TypeScript per la gestione del magazzino Scouthub, integrato con Keycloak", + "engines": { + "node": ">=20" + }, + "scripts": { + "dev": "ts-node-dev --respawn --transpile-only src/server.ts", + "build": "tsc -p tsconfig.json", + "start": "node dist/server.js", + "prisma:generate": "prisma generate", + "prisma:migrate": "prisma migrate dev", + "test": "jest --runInBand --passWithNoTests", + "test:watch": "jest --watch --runInBand" + }, + "dependencies": { + "@prisma/client": "^5.16.1", + "cors": "^2.8.5", + "dotenv": "^16.4.5", + "express": "^4.19.2", + "jsonwebtoken": "^9.0.3", + "jwks-rsa": "^3.2.2" + }, + "devDependencies": { + "@types/cors": "^2.8.17", + "@types/express": "^4.17.21", + "@types/jest": "^29.5.12", + "@types/jsonwebtoken": "^9.0.10", + "@types/node": "^20.14.12", + "@types/supertest": "^7.2.1", + "jest": "^29.7.0", + "nock": "^13.5.4", + "prisma": "^5.16.1", + "supertest": "^7.2.2", + "ts-jest": "^29.2.3", + "ts-node": "^10.9.2", + "ts-node-dev": "^2.0.0", + "typescript": "^5.5.4" + } +} diff --git a/scouthub-magazzino-be/prisma/migrations/20260724074152_init/migration.sql b/scouthub-magazzino-be/prisma/migrations/20260724074152_init/migration.sql new file mode 100644 index 0000000..496ce34 --- /dev/null +++ b/scouthub-magazzino-be/prisma/migrations/20260724074152_init/migration.sql @@ -0,0 +1,125 @@ +-- CreateEnum +CREATE TYPE "stato_materiale" AS ENUM ('proposto', 'approvato', 'rifiutato'); + +-- CreateEnum +CREATE TYPE "stato_magazzino_voce" AS ENUM ('buono', 'da_riparare', 'mancante'); + +-- CreateTable +CREATE TABLE "materiale" ( + "id" TEXT NOT NULL, + "nome" TEXT NOT NULL, + "categoria" TEXT NOT NULL, + "unita_misura" TEXT NOT NULL, + "stato" "stato_materiale" NOT NULL DEFAULT 'proposto', + "proposto_da_org_id" TEXT NOT NULL, + "creato_il" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "materiale_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "tipo_evento" ( + "id" TEXT NOT NULL, + "nome" TEXT NOT NULL, + + CONSTRAINT "tipo_evento_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "lista_modello" ( + "id" TEXT NOT NULL, + "nome" TEXT NOT NULL, + "tipo_evento_id" TEXT NOT NULL, + "pubblica" BOOLEAN NOT NULL DEFAULT true, + + CONSTRAINT "lista_modello_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "lista_modello_voce" ( + "lista_modello_id" TEXT NOT NULL, + "materiale_id" TEXT NOT NULL, + "quantita" INTEGER NOT NULL, + + CONSTRAINT "lista_modello_voce_pkey" PRIMARY KEY ("lista_modello_id","materiale_id") +); + +-- CreateTable +CREATE TABLE "lista" ( + "id" TEXT NOT NULL, + "nome" TEXT NOT NULL, + "org_id" TEXT NOT NULL, + "creata_il" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "lista_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "lista_voce" ( + "lista_id" TEXT NOT NULL, + "materiale_id" TEXT NOT NULL, + "quantita" INTEGER NOT NULL, + + CONSTRAINT "lista_voce_pkey" PRIMARY KEY ("lista_id","materiale_id") +); + +-- CreateTable +CREATE TABLE "magazzino_voce" ( + "id" TEXT NOT NULL, + "org_id" TEXT NOT NULL, + "materiale_id" TEXT NOT NULL, + "quantita_posseduta" INTEGER NOT NULL, + "stato" "stato_magazzino_voce" NOT NULL, + "posizione" TEXT, + "note" TEXT, + + CONSTRAINT "magazzino_voce_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "evento" ( + "id" TEXT NOT NULL, + "org_id" TEXT NOT NULL, + "nome" TEXT NOT NULL, + "lista_id" TEXT NOT NULL, + "data" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "evento_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "evento_check" ( + "evento_id" TEXT NOT NULL, + "materiale_id" TEXT NOT NULL, + "portato" BOOLEAN NOT NULL DEFAULT false, + "note" TEXT, + + CONSTRAINT "evento_check_pkey" PRIMARY KEY ("evento_id","materiale_id") +); + +-- AddForeignKey +ALTER TABLE "lista_modello" ADD CONSTRAINT "lista_modello_tipo_evento_id_fkey" FOREIGN KEY ("tipo_evento_id") REFERENCES "tipo_evento"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "lista_modello_voce" ADD CONSTRAINT "lista_modello_voce_lista_modello_id_fkey" FOREIGN KEY ("lista_modello_id") REFERENCES "lista_modello"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "lista_modello_voce" ADD CONSTRAINT "lista_modello_voce_materiale_id_fkey" FOREIGN KEY ("materiale_id") REFERENCES "materiale"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "lista_voce" ADD CONSTRAINT "lista_voce_lista_id_fkey" FOREIGN KEY ("lista_id") REFERENCES "lista"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "lista_voce" ADD CONSTRAINT "lista_voce_materiale_id_fkey" FOREIGN KEY ("materiale_id") REFERENCES "materiale"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "magazzino_voce" ADD CONSTRAINT "magazzino_voce_materiale_id_fkey" FOREIGN KEY ("materiale_id") REFERENCES "materiale"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "evento" ADD CONSTRAINT "evento_lista_id_fkey" FOREIGN KEY ("lista_id") REFERENCES "lista"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "evento_check" ADD CONSTRAINT "evento_check_evento_id_fkey" FOREIGN KEY ("evento_id") REFERENCES "evento"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "evento_check" ADD CONSTRAINT "evento_check_materiale_id_fkey" FOREIGN KEY ("materiale_id") REFERENCES "materiale"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/scouthub-magazzino-be/prisma/migrations/migration_lock.toml b/scouthub-magazzino-be/prisma/migrations/migration_lock.toml new file mode 100644 index 0000000..fbffa92 --- /dev/null +++ b/scouthub-magazzino-be/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (i.e. Git) +provider = "postgresql" \ No newline at end of file diff --git a/scouthub-magazzino-be/prisma/schema.prisma b/scouthub-magazzino-be/prisma/schema.prisma new file mode 100644 index 0000000..5c94583 --- /dev/null +++ b/scouthub-magazzino-be/prisma/schema.prisma @@ -0,0 +1,138 @@ +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +enum StatoMateriale { + proposto + approvato + rifiutato + + @@map("stato_materiale") +} + +enum StatoMagazzinoVoce { + buono + da_riparare + mancante + + @@map("stato_magazzino_voce") +} + +model Materiale { + id String @id @default(uuid()) + nome String + categoria String + unitaMisura String @map("unita_misura") + stato StatoMateriale @default(proposto) + propostoDaOrgId String @map("proposto_da_org_id") + creatoIl DateTime @default(now()) @map("creato_il") + + listaModelloVoci ListaModelloVoce[] + listaVoci ListaVoce[] + magazzinoVoci MagazzinoVoce[] + eventoCheck EventoCheck[] + + @@map("materiale") +} + +model TipoEvento { + id String @id @default(uuid()) + nome String + + listeModello ListaModello[] + + @@map("tipo_evento") +} + +model ListaModello { + id String @id @default(uuid()) + nome String + tipoEventoId String @map("tipo_evento_id") + pubblica Boolean @default(true) + + tipoEvento TipoEvento @relation(fields: [tipoEventoId], references: [id]) + voci ListaModelloVoce[] + + @@map("lista_modello") +} + +model ListaModelloVoce { + listaModelloId String @map("lista_modello_id") + materialeId String @map("materiale_id") + quantita Int + + listaModello ListaModello @relation(fields: [listaModelloId], references: [id]) + materiale Materiale @relation(fields: [materialeId], references: [id]) + + @@id([listaModelloId, materialeId]) + @@map("lista_modello_voce") +} + +model Lista { + id String @id @default(uuid()) + nome String + orgId String @map("org_id") + creataIl DateTime @default(now()) @map("creata_il") + + voci ListaVoce[] + eventi Evento[] + + @@map("lista") +} + +model ListaVoce { + listaId String @map("lista_id") + materialeId String @map("materiale_id") + quantita Int + + lista Lista @relation(fields: [listaId], references: [id]) + materiale Materiale @relation(fields: [materialeId], references: [id]) + + @@id([listaId, materialeId]) + @@map("lista_voce") +} + +model MagazzinoVoce { + id String @id @default(uuid()) + orgId String @map("org_id") + materialeId String @map("materiale_id") + quantitaPosseduta Int @map("quantita_posseduta") + stato StatoMagazzinoVoce + posizione String? + note String? + + materiale Materiale @relation(fields: [materialeId], references: [id]) + + @@map("magazzino_voce") +} + +model Evento { + id String @id @default(uuid()) + orgId String @map("org_id") + nome String + listaId String @map("lista_id") + data DateTime + + lista Lista @relation(fields: [listaId], references: [id]) + check EventoCheck[] + + @@map("evento") +} + +model EventoCheck { + eventoId String @map("evento_id") + materialeId String @map("materiale_id") + portato Boolean @default(false) + note String? + + evento Evento @relation(fields: [eventoId], references: [id]) + materiale Materiale @relation(fields: [materialeId], references: [id]) + + @@id([eventoId, materialeId]) + @@map("evento_check") +} diff --git a/scouthub-magazzino-be/src/app.ts b/scouthub-magazzino-be/src/app.ts new file mode 100644 index 0000000..6a2e398 --- /dev/null +++ b/scouthub-magazzino-be/src/app.ts @@ -0,0 +1,29 @@ +import express from 'express'; +import cors from 'cors'; +import { healthRouter } from './routes/health.routes'; +import { materialiRouter } from './routes/materiali.routes'; +import { tipiEventoRouter } from './routes/tipiEvento.routes'; +import { listeModelloRouter } from './routes/listeModello.routes'; +import { listeRouter } from './routes/liste.routes'; +import { magazzinoRouter } from './routes/magazzino.routes'; +import { eventiRouter } from './routes/eventi.routes'; +import { errorHandler } from './middleware/errorHandler'; + +export const app = express(); + +app.use(express.json()); +app.use(cors()); + +app.use(healthRouter); +app.use(materialiRouter); +app.use(tipiEventoRouter); +app.use(listeModelloRouter); +app.use(listeRouter); +app.use(magazzinoRouter); +app.use(eventiRouter); + +app.use((req, res) => { + res.status(404).json({ message: 'not found' }); +}); + +app.use(errorHandler); diff --git a/scouthub-magazzino-be/src/auth/auth.types.ts b/scouthub-magazzino-be/src/auth/auth.types.ts new file mode 100644 index 0000000..245c91b --- /dev/null +++ b/scouthub-magazzino-be/src/auth/auth.types.ts @@ -0,0 +1,6 @@ +export interface AuthContext { + userId: string; + email: string | null; + orgId: string | null; + roles: string[]; +} diff --git a/scouthub-magazzino-be/src/auth/require-moderatore.middleware.ts b/scouthub-magazzino-be/src/auth/require-moderatore.middleware.ts new file mode 100644 index 0000000..6286897 --- /dev/null +++ b/scouthub-magazzino-be/src/auth/require-moderatore.middleware.ts @@ -0,0 +1,15 @@ +import { Request, Response, NextFunction } from 'express'; + +// Guard per le route di moderazione (es. approvazione/rifiuto di un materiale +// proposto nel catalogo). Da usare dopo verifyToken, solo su quelle route: il +// possesso del ruolo realm moderatore non è richiesto altrove. +export function requireModeratore(req: Request, res: Response, next: NextFunction): void { + const roles = req.auth?.roles ?? []; + + if (!roles.includes('moderatore')) { + res.status(403).json({ message: 'Ruolo moderatore richiesto' }); + return; + } + + next(); +} diff --git a/scouthub-magazzino-be/src/auth/require-org-id.middleware.ts b/scouthub-magazzino-be/src/auth/require-org-id.middleware.ts new file mode 100644 index 0000000..07df1ab --- /dev/null +++ b/scouthub-magazzino-be/src/auth/require-org-id.middleware.ts @@ -0,0 +1,14 @@ +import { Request, Response, NextFunction } from 'express'; + +// Da usare dopo verifyToken su tutte le route private (liste, magazzino, eventi): +// richiede che il token porti un'organizzazione attiva. L'org_id da usare per +// filtrare le query è sempre req.auth.orgId, mai un org_id letto da +// params/query/body della richiesta. +export function requireOrgId(req: Request, res: Response, next: NextFunction): void { + if (!req.auth?.orgId) { + res.status(403).json({ message: "Nessuna organizzazione attiva sul token" }); + return; + } + + next(); +} diff --git a/scouthub-magazzino-be/src/auth/verify-token.middleware.ts b/scouthub-magazzino-be/src/auth/verify-token.middleware.ts new file mode 100644 index 0000000..f758ab4 --- /dev/null +++ b/scouthub-magazzino-be/src/auth/verify-token.middleware.ts @@ -0,0 +1,73 @@ +import { Request, Response, NextFunction } from 'express'; +import jwt, { JwtPayload } from 'jsonwebtoken'; +import jwksClient from 'jwks-rsa'; +import { env } from '../config/env'; +import { AuthContext } from './auth.types'; + +const client = jwksClient({ + jwksUri: `${env.keycloak.baseUrl}/realms/${env.keycloak.realm}/protocol/openid-connect/certs`, + cache: true, + rateLimit: true, +}); + +interface KeycloakTokenPayload extends JwtPayload { + sub: string; + email?: string; + realm_access?: { roles?: string[] }; + // Claim iniettato dalla feature "organizations" di Keycloak: mappa alias + // organizzazione -> { id, roles dell'utente in quella organizzazione }. + // Un token porta al più un'organizzazione attiva per volta. + organization?: Record; +} + +function extractBearerToken(req: Request): string | null { + const header = req.headers.authorization; + if (!header) { + return null; + } + const [scheme, token] = header.split(' '); + if (scheme !== 'Bearer' || !token) { + return null; + } + return token; +} + +function buildAuthContext(payload: KeycloakTokenPayload): AuthContext { + const realmRoles = payload.realm_access?.roles ?? []; + const [organization] = payload.organization ? Object.values(payload.organization) : []; + const orgRoles = organization?.roles ?? []; + + return { + userId: payload.sub, + email: payload.email ?? null, + orgId: organization?.id ?? null, + roles: Array.from(new Set([...realmRoles, ...orgRoles])), + }; +} + +export async function verifyToken(req: Request, res: Response, next: NextFunction): Promise { + const token = extractBearerToken(req); + if (!token) { + res.status(401).json({ message: 'Token mancante' }); + return; + } + + try { + const decoded = jwt.decode(token, { complete: true }); + if (!decoded || !decoded.header.kid) { + throw new Error("Header del token privo di 'kid'"); + } + + const signingKey = await client.getSigningKey(decoded.header.kid); + const payload = jwt.verify(token, signingKey.getPublicKey(), { algorithms: ['RS256'] }); + + if (typeof payload === 'string') { + throw new Error('Payload del token non valido'); + } + + req.auth = buildAuthContext(payload as KeycloakTokenPayload); + next(); + } catch { + res.status(401).json({ message: 'Token non valido' }); + } +} diff --git a/scouthub-magazzino-be/src/config/env.ts b/scouthub-magazzino-be/src/config/env.ts new file mode 100644 index 0000000..76995b0 --- /dev/null +++ b/scouthub-magazzino-be/src/config/env.ts @@ -0,0 +1,22 @@ +import dotenv from 'dotenv'; + +dotenv.config(); + +function requireEnv(name: string): string { + const value = process.env[name]; + if (!value) { + throw new Error(`Variabile d'ambiente mancante: ${name}`); + } + return value; +} + +export const env = { + port: Number(process.env.PORT) || 8083, + databaseUrl: requireEnv('DATABASE_URL'), + keycloak: { + baseUrl: requireEnv('KEYCLOAK_BASE_URL'), + realm: requireEnv('KEYCLOAK_REALM'), + magazzinoClientId: requireEnv('KEYCLOAK_MAGAZZINO_CLIENT_ID'), + magazzinoClientSecret: requireEnv('KEYCLOAK_MAGAZZINO_CLIENT_SECRET'), + }, +}; diff --git a/scouthub-magazzino-be/src/controllers/eventi.controller.ts b/scouthub-magazzino-be/src/controllers/eventi.controller.ts new file mode 100644 index 0000000..7aea2fd --- /dev/null +++ b/scouthub-magazzino-be/src/controllers/eventi.controller.ts @@ -0,0 +1,94 @@ +import { Request, Response, NextFunction } from 'express'; +import { AggiornaCheckInput, aggiornaCheckEvento, creaEvento, getDettaglioEvento } from '../services/eventi.service'; +import { HttpError } from '../errors'; + +interface PostEventoBody { + nome?: unknown; + listaId?: unknown; + data?: unknown; +} + +function parseData(value: unknown): Date { + if (typeof value !== 'string') { + throw new HttpError(400, "Il campo 'data' è obbligatorio ed è una stringa in formato data"); + } + const parsed = new Date(value); + if (Number.isNaN(parsed.getTime())) { + throw new HttpError(400, "Il campo 'data' non è una data valida"); + } + return parsed; +} + +function parseCreateBody(body: PostEventoBody): { nome: string; listaId: string; data: Date } { + if (typeof body.nome !== 'string' || body.nome.trim().length === 0) { + throw new HttpError(400, "Il campo 'nome' è obbligatorio ed è una stringa non vuota"); + } + if (typeof body.listaId !== 'string' || body.listaId.trim().length === 0) { + throw new HttpError(400, "Il campo 'listaId' è obbligatorio ed è una stringa non vuota"); + } + + return { nome: body.nome, listaId: body.listaId, data: parseData(body.data) }; +} + +export async function postEvento(req: Request, res: Response, next: NextFunction): Promise { + try { + const input = parseCreateBody(req.body ?? {}); + const evento = await creaEvento(req.auth!.orgId!, input); + res.status(201).json(evento); + } catch (err) { + next(err); + } +} + +export async function getEvento(req: Request, res: Response, next: NextFunction): Promise { + try { + const evento = await getDettaglioEvento(req.params.id, req.auth!.orgId!); + res.status(200).json(evento); + } catch (err) { + next(err); + } +} + +interface CheckVoceBody { + materialeId?: unknown; + portato?: unknown; + note?: unknown; +} + +interface PatchCheckBody { + voci?: unknown; +} + +function parseCheckBody(body: PatchCheckBody): AggiornaCheckInput[] { + if (!Array.isArray(body.voci) || body.voci.length === 0) { + throw new HttpError(400, "Il campo 'voci' è obbligatorio ed è un array non vuoto"); + } + + return body.voci.map((voce: CheckVoceBody) => { + if (typeof voce.materialeId !== 'string' || voce.materialeId.trim().length === 0) { + throw new HttpError(400, "Ogni voce deve avere un 'materialeId' valido"); + } + if (voce.portato !== undefined && typeof voce.portato !== 'boolean') { + throw new HttpError(400, "Il campo 'portato', se presente, deve essere un booleano"); + } + if (voce.note !== undefined && voce.note !== null && typeof voce.note !== 'string') { + throw new HttpError(400, "Il campo 'note', se presente, deve essere una stringa o null"); + } + + return { + materialeId: voce.materialeId, + portato: voce.portato as boolean | undefined, + note: voce.note as string | null | undefined, + }; + }); +} + +export async function patchEventoCheck(req: Request, res: Response, next: NextFunction): Promise { + try { + const voci = parseCheckBody(req.body ?? {}); + const evento = await aggiornaCheckEvento(req.params.id, req.auth!.orgId!, voci); + res.status(200).json(evento); + } catch (err) { + next(err); + } +} diff --git a/scouthub-magazzino-be/src/controllers/health.controller.ts b/scouthub-magazzino-be/src/controllers/health.controller.ts new file mode 100644 index 0000000..d15aa5a --- /dev/null +++ b/scouthub-magazzino-be/src/controllers/health.controller.ts @@ -0,0 +1,11 @@ +import { Request, Response, NextFunction } from 'express'; +import { healthService } from '../services/health.service'; + +export async function getHealth(req: Request, res: Response, next: NextFunction): Promise { + try { + await healthService.checkDatabase(); + res.json({ status: 'ok', database: 'up' }); + } catch (err) { + next(err); + } +} diff --git a/scouthub-magazzino-be/src/controllers/liste.controller.ts b/scouthub-magazzino-be/src/controllers/liste.controller.ts new file mode 100644 index 0000000..56dec36 --- /dev/null +++ b/scouthub-magazzino-be/src/controllers/liste.controller.ts @@ -0,0 +1,103 @@ +import { Request, Response, NextFunction } from 'express'; +import { ListaVoceInput } from '../repositories/liste.repository'; +import { + aggiornaLista, + creaListaVuota, + eliminaLista, + forkListaDaModello, + listListePerOrg, +} from '../services/liste.service'; +import { HttpError } from '../errors'; + +interface VoceBody { + materialeId?: unknown; + quantita?: unknown; +} + +function parseVoci(voci: unknown): ListaVoceInput[] { + if (!Array.isArray(voci)) { + throw new HttpError(400, "Il campo 'voci' deve essere un array"); + } + + return voci.map((voce: VoceBody) => { + if (typeof voce.materialeId !== 'string' || voce.materialeId.trim().length === 0) { + throw new HttpError(400, "Ogni voce deve avere un 'materialeId' valido"); + } + if (typeof voce.quantita !== 'number' || !Number.isInteger(voce.quantita) || voce.quantita <= 0) { + throw new HttpError(400, "Ogni voce deve avere una 'quantita' intera positiva"); + } + return { materialeId: voce.materialeId, quantita: voce.quantita }; + }); +} + +function parseNome(body: { nome?: unknown }): string { + if (typeof body.nome !== 'string' || body.nome.trim().length === 0) { + throw new HttpError(400, "Il campo 'nome' è obbligatorio ed è una stringa non vuota"); + } + return body.nome; +} + +// L'org di appartenenza è sempre req.auth.orgId (garantito da requireOrgId a +// monte): nessun org_id letto dal body/query del client viene mai usato qui. +export async function getListe(req: Request, res: Response, next: NextFunction): Promise { + try { + const liste = await listListePerOrg(req.auth!.orgId!); + res.status(200).json(liste); + } catch (err) { + next(err); + } +} + +export async function postLista(req: Request, res: Response, next: NextFunction): Promise { + try { + const nome = parseNome(req.body ?? {}); + const lista = await creaListaVuota(req.auth!.orgId!, nome); + res.status(201).json(lista); + } catch (err) { + next(err); + } +} + +export async function postListaDaModello(req: Request, res: Response, next: NextFunction): Promise { + try { + const lista = await forkListaDaModello(req.auth!.orgId!, req.params.listaModelloId); + res.status(201).json(lista); + } catch (err) { + next(err); + } +} + +interface PutListaBody { + nome?: unknown; + voci?: unknown; +} + +function parseUpdateBody(body: PutListaBody): { nome?: string; voci?: ListaVoceInput[] } { + if (body.nome !== undefined && (typeof body.nome !== 'string' || body.nome.trim().length === 0)) { + throw new HttpError(400, "Il campo 'nome', se presente, deve essere una stringa non vuota"); + } + + return { + nome: body.nome as string | undefined, + voci: body.voci !== undefined ? parseVoci(body.voci) : undefined, + }; +} + +export async function putLista(req: Request, res: Response, next: NextFunction): Promise { + try { + const input = parseUpdateBody(req.body ?? {}); + const lista = await aggiornaLista(req.params.id, req.auth!.orgId!, input); + res.status(200).json(lista); + } catch (err) { + next(err); + } +} + +export async function deleteLista(req: Request, res: Response, next: NextFunction): Promise { + try { + await eliminaLista(req.params.id, req.auth!.orgId!); + res.status(204).send(); + } catch (err) { + next(err); + } +} diff --git a/scouthub-magazzino-be/src/controllers/listeModello.controller.ts b/scouthub-magazzino-be/src/controllers/listeModello.controller.ts new file mode 100644 index 0000000..b2454e6 --- /dev/null +++ b/scouthub-magazzino-be/src/controllers/listeModello.controller.ts @@ -0,0 +1,111 @@ +import { Request, Response, NextFunction } from 'express'; +import { ListaModelloVoceInput } from '../repositories/listeModello.repository'; +import { + aggiornaListaModello, + creaListaModello, + eliminaListaModello, + listListeModello, +} from '../services/listeModello.service'; +import { HttpError } from '../errors'; + +interface VoceBody { + materialeId?: unknown; + quantita?: unknown; +} + +function parseVoci(voci: unknown): ListaModelloVoceInput[] { + if (!Array.isArray(voci)) { + throw new HttpError(400, "Il campo 'voci' deve essere un array"); + } + + return voci.map((voce: VoceBody) => { + if (typeof voce.materialeId !== 'string' || voce.materialeId.trim().length === 0) { + throw new HttpError(400, "Ogni voce deve avere un 'materialeId' valido"); + } + if (typeof voce.quantita !== 'number' || !Number.isInteger(voce.quantita) || voce.quantita <= 0) { + throw new HttpError(400, "Ogni voce deve avere una 'quantita' intera positiva"); + } + return { materialeId: voce.materialeId, quantita: voce.quantita }; + }); +} + +interface PostListaModelloBody { + nome?: unknown; + tipoEventoId?: unknown; + voci?: unknown; +} + +function parseCreateBody(body: PostListaModelloBody): { nome: string; tipoEventoId: string; voci: ListaModelloVoceInput[] } { + if (typeof body.nome !== 'string' || body.nome.trim().length === 0) { + throw new HttpError(400, "Il campo 'nome' è obbligatorio ed è una stringa non vuota"); + } + if (typeof body.tipoEventoId !== 'string' || body.tipoEventoId.trim().length === 0) { + throw new HttpError(400, "Il campo 'tipoEventoId' è obbligatorio ed è una stringa non vuota"); + } + + return { nome: body.nome, tipoEventoId: body.tipoEventoId, voci: parseVoci(body.voci ?? []) }; +} + +interface PutListaModelloBody { + nome?: unknown; + tipoEventoId?: unknown; + voci?: unknown; +} + +function parseUpdateBody(body: PutListaModelloBody): { nome?: string; tipoEventoId?: string; voci?: ListaModelloVoceInput[] } { + if (body.nome !== undefined && (typeof body.nome !== 'string' || body.nome.trim().length === 0)) { + throw new HttpError(400, "Il campo 'nome', se presente, deve essere una stringa non vuota"); + } + if (body.tipoEventoId !== undefined && (typeof body.tipoEventoId !== 'string' || body.tipoEventoId.trim().length === 0)) { + throw new HttpError(400, "Il campo 'tipoEventoId', se presente, deve essere una stringa non vuota"); + } + + return { + nome: body.nome as string | undefined, + tipoEventoId: body.tipoEventoId as string | undefined, + voci: body.voci !== undefined ? parseVoci(body.voci) : undefined, + }; +} + +// Query pubblica: unico filtro accettato è "tipoEventoId". "pubblica" non è mai +// un parametro esposto al client: le liste modello sono per definizione pubbliche. +export async function getListeModello(req: Request, res: Response, next: NextFunction): Promise { + try { + const { tipoEventoId } = req.query; + const filtro = typeof tipoEventoId === 'string' && tipoEventoId.trim().length > 0 ? tipoEventoId : undefined; + + const liste = await listListeModello(filtro); + res.status(200).json(liste); + } catch (err) { + next(err); + } +} + +export async function postListaModello(req: Request, res: Response, next: NextFunction): Promise { + try { + const input = parseCreateBody(req.body ?? {}); + const lista = await creaListaModello(input); + res.status(201).json(lista); + } catch (err) { + next(err); + } +} + +export async function putListaModello(req: Request, res: Response, next: NextFunction): Promise { + try { + const input = parseUpdateBody(req.body ?? {}); + const lista = await aggiornaListaModello(req.params.id, input); + res.status(200).json(lista); + } catch (err) { + next(err); + } +} + +export async function deleteListaModello(req: Request, res: Response, next: NextFunction): Promise { + try { + await eliminaListaModello(req.params.id); + res.status(204).send(); + } catch (err) { + next(err); + } +} diff --git a/scouthub-magazzino-be/src/controllers/magazzino.controller.ts b/scouthub-magazzino-be/src/controllers/magazzino.controller.ts new file mode 100644 index 0000000..4b17105 --- /dev/null +++ b/scouthub-magazzino-be/src/controllers/magazzino.controller.ts @@ -0,0 +1,121 @@ +import { Request, Response, NextFunction } from 'express'; +import { StatoMagazzinoVoce } from '@prisma/client'; +import { AggiornaVoceInput, AggiungiVoceInput, aggiornaVoce, aggiungiVoce, eliminaVoce, listMagazzinoPerOrg } from '../services/magazzino.service'; +import { HttpError } from '../errors'; + +const STATI_VALIDI = Object.values(StatoMagazzinoVoce); + +function isStatoValido(value: unknown): value is StatoMagazzinoVoce { + return typeof value === 'string' && (STATI_VALIDI as string[]).includes(value); +} + +interface PostVoceBody { + materialeId?: unknown; + quantitaPosseduta?: unknown; + stato?: unknown; + posizione?: unknown; + note?: unknown; +} + +function parseCreateBody(body: PostVoceBody): AggiungiVoceInput { + if (typeof body.materialeId !== 'string' || body.materialeId.trim().length === 0) { + throw new HttpError(400, "Il campo 'materialeId' è obbligatorio ed è una stringa non vuota"); + } + if (typeof body.quantitaPosseduta !== 'number' || !Number.isInteger(body.quantitaPosseduta) || body.quantitaPosseduta < 0) { + throw new HttpError(400, "Il campo 'quantitaPosseduta' è obbligatorio ed è un intero >= 0"); + } + if (!isStatoValido(body.stato)) { + throw new HttpError(400, `Il campo 'stato' deve valere uno tra: ${STATI_VALIDI.join(', ')}`); + } + if (body.posizione !== undefined && typeof body.posizione !== 'string') { + throw new HttpError(400, "Il campo 'posizione', se presente, deve essere una stringa"); + } + if (body.note !== undefined && typeof body.note !== 'string') { + throw new HttpError(400, "Il campo 'note', se presente, deve essere una stringa"); + } + + return { + materialeId: body.materialeId, + quantitaPosseduta: body.quantitaPosseduta, + stato: body.stato, + posizione: body.posizione as string | undefined, + note: body.note as string | undefined, + }; +} + +interface PutVoceBody { + materialeId?: unknown; + quantitaPosseduta?: unknown; + stato?: unknown; + posizione?: unknown; + note?: unknown; +} + +function parseUpdateBody(body: PutVoceBody): AggiornaVoceInput { + if (body.materialeId !== undefined && (typeof body.materialeId !== 'string' || body.materialeId.trim().length === 0)) { + throw new HttpError(400, "Il campo 'materialeId', se presente, deve essere una stringa non vuota"); + } + if ( + body.quantitaPosseduta !== undefined && + (typeof body.quantitaPosseduta !== 'number' || !Number.isInteger(body.quantitaPosseduta) || body.quantitaPosseduta < 0) + ) { + throw new HttpError(400, "Il campo 'quantitaPosseduta', se presente, deve essere un intero >= 0"); + } + if (body.stato !== undefined && !isStatoValido(body.stato)) { + throw new HttpError(400, `Il campo 'stato', se presente, deve valere uno tra: ${STATI_VALIDI.join(', ')}`); + } + if (body.posizione !== undefined && body.posizione !== null && typeof body.posizione !== 'string') { + throw new HttpError(400, "Il campo 'posizione', se presente, deve essere una stringa o null"); + } + if (body.note !== undefined && body.note !== null && typeof body.note !== 'string') { + throw new HttpError(400, "Il campo 'note', se presente, deve essere una stringa o null"); + } + + return { + materialeId: body.materialeId as string | undefined, + quantitaPosseduta: body.quantitaPosseduta as number | undefined, + stato: body.stato as StatoMagazzinoVoce | undefined, + posizione: body.posizione as string | null | undefined, + note: body.note as string | null | undefined, + }; +} + +// L'org di appartenenza è sempre req.auth.orgId (garantito da requireOrgId a +// monte): nessun org_id letto dal body/query del client viene mai usato qui. +export async function getMagazzino(req: Request, res: Response, next: NextFunction): Promise { + try { + const voci = await listMagazzinoPerOrg(req.auth!.orgId!); + res.status(200).json(voci); + } catch (err) { + next(err); + } +} + +export async function postVoceMagazzino(req: Request, res: Response, next: NextFunction): Promise { + try { + const input = parseCreateBody(req.body ?? {}); + const voce = await aggiungiVoce(req.auth!.orgId!, input); + res.status(201).json(voce); + } catch (err) { + next(err); + } +} + +export async function putVoceMagazzino(req: Request, res: Response, next: NextFunction): Promise { + try { + const input = parseUpdateBody(req.body ?? {}); + const voce = await aggiornaVoce(req.params.id, req.auth!.orgId!, input); + res.status(200).json(voce); + } catch (err) { + next(err); + } +} + +export async function deleteVoceMagazzino(req: Request, res: Response, next: NextFunction): Promise { + try { + await eliminaVoce(req.params.id, req.auth!.orgId!); + res.status(204).send(); + } catch (err) { + next(err); + } +} diff --git a/scouthub-magazzino-be/src/controllers/materiali.controller.ts b/scouthub-magazzino-be/src/controllers/materiali.controller.ts new file mode 100644 index 0000000..e283e82 --- /dev/null +++ b/scouthub-magazzino-be/src/controllers/materiali.controller.ts @@ -0,0 +1,84 @@ +import { Request, Response, NextFunction } from 'express'; +import { + DecisioneProposta, + decidiProposta, + listMaterialiApprovati, + listProposte, + proponiMateriale, +} from '../services/materiali.service'; +import { HttpError } from '../errors'; + +interface PostPropostaBody { + nome?: unknown; + categoria?: unknown; + unitaMisura?: unknown; +} + +function parsePropostaBody(body: PostPropostaBody): { nome: string; categoria: string; unitaMisura: string } { + if (typeof body.nome !== 'string' || body.nome.trim().length === 0) { + throw new HttpError(400, "Il campo 'nome' è obbligatorio ed è una stringa non vuota"); + } + if (typeof body.categoria !== 'string' || body.categoria.trim().length === 0) { + throw new HttpError(400, "Il campo 'categoria' è obbligatorio ed è una stringa non vuota"); + } + if (typeof body.unitaMisura !== 'string' || body.unitaMisura.trim().length === 0) { + throw new HttpError(400, "Il campo 'unitaMisura' è obbligatorio ed è una stringa non vuota"); + } + + return { nome: body.nome, categoria: body.categoria, unitaMisura: body.unitaMisura }; +} + +interface PatchPropostaBody { + decisione?: unknown; +} + +function parseDecisioneBody(body: PatchPropostaBody): DecisioneProposta { + if (body.decisione !== 'approvato' && body.decisione !== 'rifiutato') { + throw new HttpError(400, "Il campo 'decisione' deve valere 'approvato' o 'rifiutato'"); + } + return body.decisione; +} + +// Query pubblica: unico filtro accettato dal client è "categoria". Lo stato +// non è mai un parametro esposto: il catalogo pubblico mostra solo i +// materiali con stato "approvato". +export async function getMaterialiPubblici(req: Request, res: Response, next: NextFunction): Promise { + try { + const { categoria } = req.query; + const filtro = typeof categoria === 'string' && categoria.trim().length > 0 ? categoria : undefined; + + const materiali = await listMaterialiApprovati(filtro); + res.status(200).json(materiali); + } catch (err) { + next(err); + } +} + +export async function postProposta(req: Request, res: Response, next: NextFunction): Promise { + try { + const input = parsePropostaBody(req.body ?? {}); + const proposta = await proponiMateriale({ ...input, orgId: req.auth!.orgId! }); + res.status(201).json(proposta); + } catch (err) { + next(err); + } +} + +export async function getProposte(_req: Request, res: Response, next: NextFunction): Promise { + try { + const proposte = await listProposte(); + res.status(200).json(proposte); + } catch (err) { + next(err); + } +} + +export async function patchProposta(req: Request, res: Response, next: NextFunction): Promise { + try { + const decisione = parseDecisioneBody(req.body ?? {}); + const proposta = await decidiProposta(req.params.id, decisione); + res.status(200).json(proposta); + } catch (err) { + next(err); + } +} diff --git a/scouthub-magazzino-be/src/controllers/tipiEvento.controller.ts b/scouthub-magazzino-be/src/controllers/tipiEvento.controller.ts new file mode 100644 index 0000000..fa05461 --- /dev/null +++ b/scouthub-magazzino-be/src/controllers/tipiEvento.controller.ts @@ -0,0 +1,48 @@ +import { Request, Response, NextFunction } from 'express'; +import { aggiornaTipoEvento, creaTipoEvento, eliminaTipoEvento, listTipiEvento } from '../services/tipiEvento.service'; +import { HttpError } from '../errors'; + +function parseNome(body: { nome?: unknown }): string { + if (typeof body.nome !== 'string' || body.nome.trim().length === 0) { + throw new HttpError(400, "Il campo 'nome' è obbligatorio ed è una stringa non vuota"); + } + return body.nome; +} + +export async function getTipiEvento(_req: Request, res: Response, next: NextFunction): Promise { + try { + const tipiEvento = await listTipiEvento(); + res.status(200).json(tipiEvento); + } catch (err) { + next(err); + } +} + +export async function postTipoEvento(req: Request, res: Response, next: NextFunction): Promise { + try { + const nome = parseNome(req.body ?? {}); + const tipoEvento = await creaTipoEvento(nome); + res.status(201).json(tipoEvento); + } catch (err) { + next(err); + } +} + +export async function putTipoEvento(req: Request, res: Response, next: NextFunction): Promise { + try { + const nome = parseNome(req.body ?? {}); + const tipoEvento = await aggiornaTipoEvento(req.params.id, nome); + res.status(200).json(tipoEvento); + } catch (err) { + next(err); + } +} + +export async function deleteTipoEvento(req: Request, res: Response, next: NextFunction): Promise { + try { + await eliminaTipoEvento(req.params.id); + res.status(204).send(); + } catch (err) { + next(err); + } +} diff --git a/scouthub-magazzino-be/src/db/prisma.ts b/scouthub-magazzino-be/src/db/prisma.ts new file mode 100644 index 0000000..5eaa431 --- /dev/null +++ b/scouthub-magazzino-be/src/db/prisma.ts @@ -0,0 +1,12 @@ +import { PrismaClient } from '@prisma/client'; + +declare global { + // eslint-disable-next-line no-var + var __prisma: PrismaClient | undefined; +} + +export const prisma = global.__prisma ?? new PrismaClient(); + +if (process.env.NODE_ENV !== 'production') { + global.__prisma = prisma; +} diff --git a/scouthub-magazzino-be/src/errors.ts b/scouthub-magazzino-be/src/errors.ts new file mode 100644 index 0000000..f39d1a8 --- /dev/null +++ b/scouthub-magazzino-be/src/errors.ts @@ -0,0 +1,9 @@ +export class HttpError extends Error { + statusCode: number; + + constructor(statusCode: number, message: string) { + super(message); + this.statusCode = statusCode; + this.name = 'HttpError'; + } +} diff --git a/scouthub-magazzino-be/src/middleware/errorHandler.ts b/scouthub-magazzino-be/src/middleware/errorHandler.ts new file mode 100644 index 0000000..d7f3256 --- /dev/null +++ b/scouthub-magazzino-be/src/middleware/errorHandler.ts @@ -0,0 +1,10 @@ +import { Request, Response, NextFunction } from 'express'; + +export function errorHandler(err: unknown, req: Request, res: Response, next: NextFunction): void { + const hasStatusCode = + typeof err === 'object' && err !== null && 'statusCode' in err && typeof (err as { statusCode?: unknown }).statusCode === 'number'; + const statusCode = hasStatusCode ? (err as { statusCode: number }).statusCode : 500; + const message = err instanceof Error && err.message ? err.message : 'Errore interno del server'; + + res.status(statusCode).json({ message }); +} diff --git a/scouthub-magazzino-be/src/repositories/eventi.repository.ts b/scouthub-magazzino-be/src/repositories/eventi.repository.ts new file mode 100644 index 0000000..f398282 --- /dev/null +++ b/scouthub-magazzino-be/src/repositories/eventi.repository.ts @@ -0,0 +1,48 @@ +import { Prisma } from '@prisma/client'; +import { prisma } from '../db/prisma'; + +const includeEvento = { + lista: { include: { voci: { include: { materiale: true } } } }, + check: true, +} satisfies Prisma.EventoInclude; + +export type EventoConDettagli = Prisma.EventoGetPayload<{ include: typeof includeEvento }>; + +export interface CreateEventoData { + orgId: string; + nome: string; + listaId: string; + data: Date; +} + +export interface UpsertCheckData { + portato?: boolean; + note?: string | null; +} + +export class EventiRepository { + // id + orgId nella stessa where: un evento di un'altra org risulta + // semplicemente "non trovato", mai un 403 che ne rivela l'esistenza. + findByIdAndOrg(id: string, orgId: string): Promise { + return prisma.evento.findFirst({ where: { id, orgId }, include: includeEvento }); + } + + create(data: CreateEventoData): Promise { + return prisma.evento.create({ data, include: includeEvento }); + } + + upsertCheck(eventoId: string, materialeId: string, data: UpsertCheckData): Promise { + return prisma.eventoCheck + .upsert({ + where: { eventoId_materialeId: { eventoId, materialeId } }, + create: { eventoId, materialeId, portato: data.portato ?? false, note: data.note ?? null }, + update: { + ...(data.portato !== undefined ? { portato: data.portato } : {}), + ...(data.note !== undefined ? { note: data.note } : {}), + }, + }) + .then(() => undefined); + } +} + +export const eventiRepository = new EventiRepository(); diff --git a/scouthub-magazzino-be/src/repositories/health.repository.ts b/scouthub-magazzino-be/src/repositories/health.repository.ts new file mode 100644 index 0000000..7219489 --- /dev/null +++ b/scouthub-magazzino-be/src/repositories/health.repository.ts @@ -0,0 +1,9 @@ +import { prisma } from '../db/prisma'; + +export class HealthRepository { + async ping(): Promise { + await prisma.$queryRaw`SELECT 1`; + } +} + +export const healthRepository = new HealthRepository(); diff --git a/scouthub-magazzino-be/src/repositories/liste.repository.ts b/scouthub-magazzino-be/src/repositories/liste.repository.ts new file mode 100644 index 0000000..7343712 --- /dev/null +++ b/scouthub-magazzino-be/src/repositories/liste.repository.ts @@ -0,0 +1,79 @@ +import { Prisma } from '@prisma/client'; +import { prisma } from '../db/prisma'; + +const includeVoci = { + voci: { include: { materiale: true } }, +} satisfies Prisma.ListaInclude; + +export type ListaConVoci = Prisma.ListaGetPayload<{ include: typeof includeVoci }>; + +export interface ListaVoceInput { + materialeId: string; + quantita: number; +} + +export interface CreateListaData { + nome: string; + orgId: string; + voci: ListaVoceInput[]; +} + +export interface UpdateListaData { + nome?: string; + voci?: ListaVoceInput[]; +} + +export class ListeRepository { + findAllByOrg(orgId: string): Promise { + return prisma.lista.findMany({ + where: { orgId }, + include: includeVoci, + orderBy: { creataIl: 'desc' }, + }); + } + + // id + orgId nella stessa where: una lista di un'altra org risulta + // semplicemente "non trovata", mai un 403 che ne rivela l'esistenza. + findByIdAndOrg(id: string, orgId: string): Promise { + return prisma.lista.findFirst({ where: { id, orgId }, include: includeVoci }); + } + + create(data: CreateListaData): Promise { + return prisma.lista.create({ + data: { + nome: data.nome, + orgId: data.orgId, + voci: { create: data.voci.map((v) => ({ materialeId: v.materialeId, quantita: v.quantita })) }, + }, + include: includeVoci, + }); + } + + update(id: string, data: UpdateListaData): Promise { + return prisma.$transaction(async (tx) => { + if (data.voci) { + await tx.listaVoce.deleteMany({ where: { listaId: id } }); + } + + return tx.lista.update({ + where: { id }, + data: { + ...(data.nome !== undefined ? { nome: data.nome } : {}), + ...(data.voci + ? { voci: { create: data.voci.map((v) => ({ materialeId: v.materialeId, quantita: v.quantita })) } } + : {}), + }, + include: includeVoci, + }); + }); + } + + async delete(id: string): Promise { + await prisma.$transaction(async (tx) => { + await tx.listaVoce.deleteMany({ where: { listaId: id } }); + await tx.lista.delete({ where: { id } }); + }); + } +} + +export const listeRepository = new ListeRepository(); diff --git a/scouthub-magazzino-be/src/repositories/listeModello.repository.ts b/scouthub-magazzino-be/src/repositories/listeModello.repository.ts new file mode 100644 index 0000000..dd957ed --- /dev/null +++ b/scouthub-magazzino-be/src/repositories/listeModello.repository.ts @@ -0,0 +1,82 @@ +import { Prisma } from '@prisma/client'; +import { prisma } from '../db/prisma'; + +const includeVoci = { + voci: { include: { materiale: true } }, +} satisfies Prisma.ListaModelloInclude; + +export type ListaModelloConVoci = Prisma.ListaModelloGetPayload<{ include: typeof includeVoci }>; + +export interface ListaModelloVoceInput { + materialeId: string; + quantita: number; +} + +export interface CreateListaModelloData { + nome: string; + tipoEventoId: string; + voci: ListaModelloVoceInput[]; +} + +export interface UpdateListaModelloData { + nome?: string; + tipoEventoId?: string; + voci?: ListaModelloVoceInput[]; +} + +export class ListeModelloRepository { + // "pubblica" è sempre true per le liste modello: non è un filtro opzionale, + // è la condizione fissa che qualifica queste liste come tali. + findAll(tipoEventoId?: string): Promise { + return prisma.listaModello.findMany({ + where: { pubblica: true, ...(tipoEventoId ? { tipoEventoId } : {}) }, + include: includeVoci, + orderBy: { nome: 'asc' }, + }); + } + + findById(id: string): Promise { + return prisma.listaModello.findUnique({ where: { id }, include: includeVoci }); + } + + create(data: CreateListaModelloData): Promise { + return prisma.listaModello.create({ + data: { + nome: data.nome, + tipoEventoId: data.tipoEventoId, + pubblica: true, + voci: { create: data.voci.map((v) => ({ materialeId: v.materialeId, quantita: v.quantita })) }, + }, + include: includeVoci, + }); + } + + update(id: string, data: UpdateListaModelloData): Promise { + return prisma.$transaction(async (tx) => { + if (data.voci) { + await tx.listaModelloVoce.deleteMany({ where: { listaModelloId: id } }); + } + + return tx.listaModello.update({ + where: { id }, + data: { + ...(data.nome !== undefined ? { nome: data.nome } : {}), + ...(data.tipoEventoId !== undefined ? { tipoEventoId: data.tipoEventoId } : {}), + ...(data.voci + ? { voci: { create: data.voci.map((v) => ({ materialeId: v.materialeId, quantita: v.quantita })) } } + : {}), + }, + include: includeVoci, + }); + }); + } + + async delete(id: string): Promise { + await prisma.$transaction(async (tx) => { + await tx.listaModelloVoce.deleteMany({ where: { listaModelloId: id } }); + await tx.listaModello.delete({ where: { id } }); + }); + } +} + +export const listeModelloRepository = new ListeModelloRepository(); diff --git a/scouthub-magazzino-be/src/repositories/magazzino.repository.ts b/scouthub-magazzino-be/src/repositories/magazzino.repository.ts new file mode 100644 index 0000000..792c88d --- /dev/null +++ b/scouthub-magazzino-be/src/repositories/magazzino.repository.ts @@ -0,0 +1,69 @@ +import { Prisma, StatoMagazzinoVoce } from '@prisma/client'; +import { prisma } from '../db/prisma'; + +const includeMateriale = { + materiale: true, +} satisfies Prisma.MagazzinoVoceInclude; + +export type MagazzinoVoceConMateriale = Prisma.MagazzinoVoceGetPayload<{ include: typeof includeMateriale }>; + +export interface CreateMagazzinoVoceData { + orgId: string; + materialeId: string; + quantitaPosseduta: number; + stato: StatoMagazzinoVoce; + posizione?: string; + note?: string; +} + +export interface UpdateMagazzinoVoceData { + materialeId?: string; + quantitaPosseduta?: number; + stato?: StatoMagazzinoVoce; + posizione?: string | null; + note?: string | null; +} + +export interface QuantitaPosseduta { + materialeId: string; + quantitaPosseduta: number; +} + +export class MagazzinoRepository { + findAllByOrg(orgId: string): Promise { + return prisma.magazzinoVoce.findMany({ + where: { orgId }, + include: includeMateriale, + orderBy: { id: 'asc' }, + }); + } + + // id + orgId nella stessa where: una voce di un'altra org risulta + // semplicemente "non trovata", mai un 403 che ne rivela l'esistenza. + findByIdAndOrg(id: string, orgId: string): Promise { + return prisma.magazzinoVoce.findFirst({ where: { id, orgId }, include: includeMateriale }); + } + + create(data: CreateMagazzinoVoceData): Promise { + return prisma.magazzinoVoce.create({ data, include: includeMateriale }); + } + + update(id: string, data: UpdateMagazzinoVoceData): Promise { + return prisma.magazzinoVoce.update({ where: { id }, data, include: includeMateriale }); + } + + async delete(id: string): Promise { + await prisma.magazzinoVoce.delete({ where: { id } }); + } + + // Usato per il join evento<->magazzino: quantità possedute dall'org per un + // sottoinsieme di materiali (quelli della lista collegata all'evento). + findQuantitaByOrgEMateriali(orgId: string, materialeIds: string[]): Promise { + return prisma.magazzinoVoce.findMany({ + where: { orgId, materialeId: { in: materialeIds } }, + select: { materialeId: true, quantitaPosseduta: true }, + }); + } +} + +export const magazzinoRepository = new MagazzinoRepository(); diff --git a/scouthub-magazzino-be/src/repositories/materiali.repository.ts b/scouthub-magazzino-be/src/repositories/materiali.repository.ts new file mode 100644 index 0000000..79d0c4f --- /dev/null +++ b/scouthub-magazzino-be/src/repositories/materiali.repository.ts @@ -0,0 +1,41 @@ +import { Materiale, StatoMateriale } from '@prisma/client'; +import { prisma } from '../db/prisma'; + +export interface CreateMaterialeData { + nome: string; + categoria: string; + unitaMisura: string; + propostoDaOrgId: string; +} + +export class MaterialiRepository { + findApprovati(categoria?: string): Promise { + return prisma.materiale.findMany({ + where: { stato: StatoMateriale.approvato, ...(categoria ? { categoria } : {}) }, + orderBy: { nome: 'asc' }, + }); + } + + findProposte(): Promise { + return prisma.materiale.findMany({ + where: { stato: StatoMateriale.proposto }, + orderBy: { creatoIl: 'asc' }, + }); + } + + findById(id: string): Promise { + return prisma.materiale.findUnique({ where: { id } }); + } + + create(data: CreateMaterialeData): Promise { + return prisma.materiale.create({ + data: { ...data, stato: StatoMateriale.proposto }, + }); + } + + updateStato(id: string, stato: StatoMateriale): Promise { + return prisma.materiale.update({ where: { id }, data: { stato } }); + } +} + +export const materialiRepository = new MaterialiRepository(); diff --git a/scouthub-magazzino-be/src/repositories/tipiEvento.repository.ts b/scouthub-magazzino-be/src/repositories/tipiEvento.repository.ts new file mode 100644 index 0000000..fb3c341 --- /dev/null +++ b/scouthub-magazzino-be/src/repositories/tipiEvento.repository.ts @@ -0,0 +1,22 @@ +import { TipoEvento } from '@prisma/client'; +import { prisma } from '../db/prisma'; + +export class TipiEventoRepository { + findAll(): Promise { + return prisma.tipoEvento.findMany({ orderBy: { nome: 'asc' } }); + } + + create(nome: string): Promise { + return prisma.tipoEvento.create({ data: { nome } }); + } + + update(id: string, nome: string): Promise { + return prisma.tipoEvento.update({ where: { id }, data: { nome } }); + } + + async delete(id: string): Promise { + await prisma.tipoEvento.delete({ where: { id } }); + } +} + +export const tipiEventoRepository = new TipiEventoRepository(); diff --git a/scouthub-magazzino-be/src/routes/eventi.routes.ts b/scouthub-magazzino-be/src/routes/eventi.routes.ts new file mode 100644 index 0000000..1da3550 --- /dev/null +++ b/scouthub-magazzino-be/src/routes/eventi.routes.ts @@ -0,0 +1,10 @@ +import { Router } from 'express'; +import { verifyToken } from '../auth/verify-token.middleware'; +import { requireOrgId } from '../auth/require-org-id.middleware'; +import { getEvento, patchEventoCheck, postEvento } from '../controllers/eventi.controller'; + +export const eventiRouter = Router(); + +eventiRouter.post('/eventi', verifyToken, requireOrgId, postEvento); +eventiRouter.get('/eventi/:id', verifyToken, requireOrgId, getEvento); +eventiRouter.patch('/eventi/:id/check', verifyToken, requireOrgId, patchEventoCheck); diff --git a/scouthub-magazzino-be/src/routes/health.routes.ts b/scouthub-magazzino-be/src/routes/health.routes.ts new file mode 100644 index 0000000..84938e3 --- /dev/null +++ b/scouthub-magazzino-be/src/routes/health.routes.ts @@ -0,0 +1,6 @@ +import { Router } from 'express'; +import { getHealth } from '../controllers/health.controller'; + +export const healthRouter = Router(); + +healthRouter.get('/health', getHealth); diff --git a/scouthub-magazzino-be/src/routes/liste.routes.ts b/scouthub-magazzino-be/src/routes/liste.routes.ts new file mode 100644 index 0000000..d455edf --- /dev/null +++ b/scouthub-magazzino-be/src/routes/liste.routes.ts @@ -0,0 +1,12 @@ +import { Router } from 'express'; +import { verifyToken } from '../auth/verify-token.middleware'; +import { requireOrgId } from '../auth/require-org-id.middleware'; +import { deleteLista, getListe, postLista, postListaDaModello, putLista } from '../controllers/liste.controller'; + +export const listeRouter = Router(); + +listeRouter.get('/liste', verifyToken, requireOrgId, getListe); +listeRouter.post('/liste', verifyToken, requireOrgId, postLista); +listeRouter.post('/liste/da-modello/:listaModelloId', verifyToken, requireOrgId, postListaDaModello); +listeRouter.put('/liste/:id', verifyToken, requireOrgId, putLista); +listeRouter.delete('/liste/:id', verifyToken, requireOrgId, deleteLista); diff --git a/scouthub-magazzino-be/src/routes/listeModello.routes.ts b/scouthub-magazzino-be/src/routes/listeModello.routes.ts new file mode 100644 index 0000000..f5cd0f1 --- /dev/null +++ b/scouthub-magazzino-be/src/routes/listeModello.routes.ts @@ -0,0 +1,16 @@ +import { Router } from 'express'; +import { verifyToken } from '../auth/verify-token.middleware'; +import { requireModeratore } from '../auth/require-moderatore.middleware'; +import { + deleteListaModello, + getListeModello, + postListaModello, + putListaModello, +} from '../controllers/listeModello.controller'; + +export const listeModelloRouter = Router(); + +listeModelloRouter.get('/liste-modello', getListeModello); +listeModelloRouter.post('/liste-modello', verifyToken, requireModeratore, postListaModello); +listeModelloRouter.put('/liste-modello/:id', verifyToken, requireModeratore, putListaModello); +listeModelloRouter.delete('/liste-modello/:id', verifyToken, requireModeratore, deleteListaModello); diff --git a/scouthub-magazzino-be/src/routes/magazzino.routes.ts b/scouthub-magazzino-be/src/routes/magazzino.routes.ts new file mode 100644 index 0000000..05ef8dd --- /dev/null +++ b/scouthub-magazzino-be/src/routes/magazzino.routes.ts @@ -0,0 +1,11 @@ +import { Router } from 'express'; +import { verifyToken } from '../auth/verify-token.middleware'; +import { requireOrgId } from '../auth/require-org-id.middleware'; +import { deleteVoceMagazzino, getMagazzino, postVoceMagazzino, putVoceMagazzino } from '../controllers/magazzino.controller'; + +export const magazzinoRouter = Router(); + +magazzinoRouter.get('/magazzino', verifyToken, requireOrgId, getMagazzino); +magazzinoRouter.post('/magazzino', verifyToken, requireOrgId, postVoceMagazzino); +magazzinoRouter.put('/magazzino/:id', verifyToken, requireOrgId, putVoceMagazzino); +magazzinoRouter.delete('/magazzino/:id', verifyToken, requireOrgId, deleteVoceMagazzino); diff --git a/scouthub-magazzino-be/src/routes/materiali.routes.ts b/scouthub-magazzino-be/src/routes/materiali.routes.ts new file mode 100644 index 0000000..d1bfb09 --- /dev/null +++ b/scouthub-magazzino-be/src/routes/materiali.routes.ts @@ -0,0 +1,12 @@ +import { Router } from 'express'; +import { verifyToken } from '../auth/verify-token.middleware'; +import { requireOrgId } from '../auth/require-org-id.middleware'; +import { requireModeratore } from '../auth/require-moderatore.middleware'; +import { getMaterialiPubblici, getProposte, patchProposta, postProposta } from '../controllers/materiali.controller'; + +export const materialiRouter = Router(); + +materialiRouter.get('/materiali', getMaterialiPubblici); +materialiRouter.post('/materiali/proposte', verifyToken, requireOrgId, postProposta); +materialiRouter.get('/materiali/proposte', verifyToken, requireModeratore, getProposte); +materialiRouter.patch('/materiali/proposte/:id', verifyToken, requireModeratore, patchProposta); diff --git a/scouthub-magazzino-be/src/routes/tipiEvento.routes.ts b/scouthub-magazzino-be/src/routes/tipiEvento.routes.ts new file mode 100644 index 0000000..2ed3907 --- /dev/null +++ b/scouthub-magazzino-be/src/routes/tipiEvento.routes.ts @@ -0,0 +1,11 @@ +import { Router } from 'express'; +import { verifyToken } from '../auth/verify-token.middleware'; +import { requireModeratore } from '../auth/require-moderatore.middleware'; +import { deleteTipoEvento, getTipiEvento, postTipoEvento, putTipoEvento } from '../controllers/tipiEvento.controller'; + +export const tipiEventoRouter = Router(); + +tipiEventoRouter.get('/tipi-evento', getTipiEvento); +tipiEventoRouter.post('/tipi-evento', verifyToken, requireModeratore, postTipoEvento); +tipiEventoRouter.put('/tipi-evento/:id', verifyToken, requireModeratore, putTipoEvento); +tipiEventoRouter.delete('/tipi-evento/:id', verifyToken, requireModeratore, deleteTipoEvento); diff --git a/scouthub-magazzino-be/src/server.ts b/scouthub-magazzino-be/src/server.ts new file mode 100644 index 0000000..7fc45f5 --- /dev/null +++ b/scouthub-magazzino-be/src/server.ts @@ -0,0 +1,6 @@ +import { app } from './app'; +import { env } from './config/env'; + +app.listen(env.port, () => { + console.log(`Server avviato su porta ${env.port}`); +}); diff --git a/scouthub-magazzino-be/src/services/eventi.service.ts b/scouthub-magazzino-be/src/services/eventi.service.ts new file mode 100644 index 0000000..c9d6157 --- /dev/null +++ b/scouthub-magazzino-be/src/services/eventi.service.ts @@ -0,0 +1,109 @@ +import { EventoConDettagli, eventiRepository } from '../repositories/eventi.repository'; +import { listeRepository } from '../repositories/liste.repository'; +import { magazzinoRepository } from '../repositories/magazzino.repository'; +import { HttpError } from '../errors'; + +export interface EventoVoceView { + materialeId: string; + nome: string; + unitaMisura: string; + quantitaRichiesta: number; + quantitaPosseduta: number; + portato: boolean; + note: string | null; +} + +export interface EventoDettaglioView { + id: string; + orgId: string; + nome: string; + listaId: string; + data: Date; + voci: EventoVoceView[]; +} + +// Join fra le voci della lista collegata all'evento e il magazzino dell'org: +// per ogni materiale della lista, quanto ne possiede l'org (0 se non tracciato) +// e lo stato di check (di default "non portato", nessuna nota) finché non +// viene aggiornato via PATCH /eventi/:id/check. +async function buildDettaglioView(evento: EventoConDettagli): Promise { + const materialeIds = evento.lista.voci.map((v) => v.materialeId); + const quantitaPossedute = + materialeIds.length > 0 ? await magazzinoRepository.findQuantitaByOrgEMateriali(evento.orgId, materialeIds) : []; + const magazzinoByMateriale = new Map(quantitaPossedute.map((m) => [m.materialeId, m.quantitaPosseduta])); + const checkByMateriale = new Map(evento.check.map((c) => [c.materialeId, c])); + + return { + id: evento.id, + orgId: evento.orgId, + nome: evento.nome, + listaId: evento.listaId, + data: evento.data, + voci: evento.lista.voci.map((v) => { + const check = checkByMateriale.get(v.materialeId); + return { + materialeId: v.materialeId, + nome: v.materiale.nome, + unitaMisura: v.materiale.unitaMisura, + quantitaRichiesta: v.quantita, + quantitaPosseduta: magazzinoByMateriale.get(v.materialeId) ?? 0, + portato: check?.portato ?? false, + note: check?.note ?? null, + }; + }), + }; +} + +export interface CreaEventoInput { + nome: string; + listaId: string; + data: Date; +} + +export async function creaEvento(orgId: string, input: CreaEventoInput): Promise { + const lista = await listeRepository.findByIdAndOrg(input.listaId, orgId); + if (!lista) { + throw new HttpError(400, 'La lista indicata non esiste o non appartiene alla tua organizzazione'); + } + + const evento = await eventiRepository.create({ orgId, nome: input.nome, listaId: input.listaId, data: input.data }); + return buildDettaglioView(evento); +} + +export async function getDettaglioEvento(id: string, orgId: string): Promise { + const evento = await eventiRepository.findByIdAndOrg(id, orgId); + if (!evento) { + throw new HttpError(404, 'Evento non trovato'); + } + return buildDettaglioView(evento); +} + +export interface AggiornaCheckInput { + materialeId: string; + portato?: boolean; + note?: string | null; +} + +export async function aggiornaCheckEvento( + id: string, + orgId: string, + voci: AggiornaCheckInput[], +): Promise { + const evento = await eventiRepository.findByIdAndOrg(id, orgId); + if (!evento) { + throw new HttpError(404, 'Evento non trovato'); + } + + const materialiDellaLista = new Set(evento.lista.voci.map((v) => v.materialeId)); + for (const voce of voci) { + if (!materialiDellaLista.has(voce.materialeId)) { + throw new HttpError(400, `Il materiale ${voce.materialeId} non fa parte della lista collegata a questo evento`); + } + } + + for (const voce of voci) { + await eventiRepository.upsertCheck(id, voce.materialeId, { portato: voce.portato, note: voce.note }); + } + + return getDettaglioEvento(id, orgId); +} diff --git a/scouthub-magazzino-be/src/services/health.service.ts b/scouthub-magazzino-be/src/services/health.service.ts new file mode 100644 index 0000000..532e30d --- /dev/null +++ b/scouthub-magazzino-be/src/services/health.service.ts @@ -0,0 +1,10 @@ +import { healthRepository } from '../repositories/health.repository'; + +export class HealthService { + async checkDatabase(): Promise { + await healthRepository.ping(); + return true; + } +} + +export const healthService = new HealthService(); diff --git a/scouthub-magazzino-be/src/services/liste.service.ts b/scouthub-magazzino-be/src/services/liste.service.ts new file mode 100644 index 0000000..e9bbeb5 --- /dev/null +++ b/scouthub-magazzino-be/src/services/liste.service.ts @@ -0,0 +1,89 @@ +import { ListaConVoci, ListaVoceInput, listeRepository } from '../repositories/liste.repository'; +import { listeModelloRepository } from '../repositories/listeModello.repository'; +import { HttpError } from '../errors'; +import { toHttpError } from '../utils/prisma-errors'; + +export interface ListaVoceView { + materialeId: string; + nome: string; + unitaMisura: string; + quantita: number; +} + +export interface ListaView { + id: string; + nome: string; + orgId: string; + creataIl: Date; + voci: ListaVoceView[]; +} + +function toView(lista: ListaConVoci): ListaView { + return { + id: lista.id, + nome: lista.nome, + orgId: lista.orgId, + creataIl: lista.creataIl, + voci: lista.voci.map((v) => ({ + materialeId: v.materialeId, + nome: v.materiale.nome, + unitaMisura: v.materiale.unitaMisura, + quantita: v.quantita, + })), + }; +} + +export async function listListePerOrg(orgId: string): Promise { + const liste = await listeRepository.findAllByOrg(orgId); + return liste.map(toView); +} + +export async function creaListaVuota(orgId: string, nome: string): Promise { + const lista = await listeRepository.create({ nome, orgId, voci: [] }); + return toView(lista); +} + +// Fork: copia nome e voci correnti della lista modello in una nuova lista +// dell'org. Da qui in poi le due entità non hanno più alcun legame: nessun +// listaModelloId viene salvato sulla lista creata. +export async function forkListaDaModello(orgId: string, listaModelloId: string): Promise { + const modello = await listeModelloRepository.findById(listaModelloId); + if (!modello || !modello.pubblica) { + throw new HttpError(404, 'Lista modello non trovata'); + } + + const voci: ListaVoceInput[] = modello.voci.map((v) => ({ materialeId: v.materialeId, quantita: v.quantita })); + const lista = await listeRepository.create({ nome: modello.nome, orgId, voci }); + return toView(lista); +} + +async function assicuraListaDiOrg(id: string, orgId: string): Promise { + const lista = await listeRepository.findByIdAndOrg(id, orgId); + if (!lista) { + throw new HttpError(404, 'Lista non trovata'); + } +} + +export interface AggiornaListaInput { + nome?: string; + voci?: ListaVoceInput[]; +} + +export async function aggiornaLista(id: string, orgId: string, input: AggiornaListaInput): Promise { + await assicuraListaDiOrg(id, orgId); + try { + const lista = await listeRepository.update(id, input); + return toView(lista); + } catch (err) { + throw toHttpError(err, 'Lista non trovata', 'Uno dei materiali indicati non esiste'); + } +} + +export async function eliminaLista(id: string, orgId: string): Promise { + await assicuraListaDiOrg(id, orgId); + try { + await listeRepository.delete(id); + } catch (err) { + throw toHttpError(err, 'Lista non trovata', 'Impossibile eliminare la lista: è referenziata da un evento'); + } +} diff --git a/scouthub-magazzino-be/src/services/listeModello.service.ts b/scouthub-magazzino-be/src/services/listeModello.service.ts new file mode 100644 index 0000000..72a27f8 --- /dev/null +++ b/scouthub-magazzino-be/src/services/listeModello.service.ts @@ -0,0 +1,81 @@ +import { + CreateListaModelloData, + ListaModelloConVoci, + ListaModelloVoceInput, + UpdateListaModelloData, + listeModelloRepository, +} from '../repositories/listeModello.repository'; +import { toHttpError } from '../utils/prisma-errors'; + +export interface ListaModelloVoceView { + materialeId: string; + nome: string; + unitaMisura: string; + quantita: number; +} + +export interface ListaModelloView { + id: string; + nome: string; + tipoEventoId: string; + voci: ListaModelloVoceView[]; +} + +function toView(lista: ListaModelloConVoci): ListaModelloView { + return { + id: lista.id, + nome: lista.nome, + tipoEventoId: lista.tipoEventoId, + voci: lista.voci.map((v) => ({ + materialeId: v.materialeId, + nome: v.materiale.nome, + unitaMisura: v.materiale.unitaMisura, + quantita: v.quantita, + })), + }; +} + +export async function listListeModello(tipoEventoId?: string): Promise { + const liste = await listeModelloRepository.findAll(tipoEventoId); + return liste.map(toView); +} + +export interface CreaListaModelloInput { + nome: string; + tipoEventoId: string; + voci: ListaModelloVoceInput[]; +} + +export async function creaListaModello(input: CreaListaModelloInput): Promise { + const data: CreateListaModelloData = input; + try { + const lista = await listeModelloRepository.create(data); + return toView(lista); + } catch (err) { + throw toHttpError(err, 'Tipo evento non trovato', 'Uno dei materiali indicati non esiste'); + } +} + +export interface AggiornaListaModelloInput { + nome?: string; + tipoEventoId?: string; + voci?: ListaModelloVoceInput[]; +} + +export async function aggiornaListaModello(id: string, input: AggiornaListaModelloInput): Promise { + const data: UpdateListaModelloData = input; + try { + const lista = await listeModelloRepository.update(id, data); + return toView(lista); + } catch (err) { + throw toHttpError(err, 'Lista modello non trovata', 'Riferimento non valido (tipo evento o materiale inesistente)'); + } +} + +export async function eliminaListaModello(id: string): Promise { + try { + await listeModelloRepository.delete(id); + } catch (err) { + throw toHttpError(err, 'Lista modello non trovata', 'Impossibile eliminare la lista modello'); + } +} diff --git a/scouthub-magazzino-be/src/services/magazzino.service.ts b/scouthub-magazzino-be/src/services/magazzino.service.ts new file mode 100644 index 0000000..7b14610 --- /dev/null +++ b/scouthub-magazzino-be/src/services/magazzino.service.ts @@ -0,0 +1,109 @@ +import { StatoMagazzinoVoce, StatoMateriale } from '@prisma/client'; +import { + CreateMagazzinoVoceData, + MagazzinoVoceConMateriale, + UpdateMagazzinoVoceData, + magazzinoRepository, +} from '../repositories/magazzino.repository'; +import { materialiRepository } from '../repositories/materiali.repository'; +import { HttpError } from '../errors'; +import { toHttpError } from '../utils/prisma-errors'; + +export interface MagazzinoVoceView { + id: string; + orgId: string; + materialeId: string; + materialeNome: string; + materialeCategoria: string; + quantitaPosseduta: number; + stato: StatoMagazzinoVoce; + posizione: string | null; + note: string | null; +} + +function toView(voce: MagazzinoVoceConMateriale): MagazzinoVoceView { + return { + id: voce.id, + orgId: voce.orgId, + materialeId: voce.materialeId, + materialeNome: voce.materiale.nome, + materialeCategoria: voce.materiale.categoria, + quantitaPosseduta: voce.quantitaPosseduta, + stato: voce.stato, + posizione: voce.posizione, + note: voce.note, + }; +} + +// Non basta che materiale_id esista: deve essere nel catalogo pubblico +// approvato. Un materiale ancora "proposto" o "rifiutato" non può finire nel +// magazzino di un gruppo. +async function assicuraMaterialeApprovato(materialeId: string): Promise { + const materiale = await materialiRepository.findById(materialeId); + if (!materiale || materiale.stato !== StatoMateriale.approvato) { + throw new HttpError( + 400, + 'Il materiale indicato non è nel catalogo pubblico approvato: proponilo prima tramite POST /materiali/proposte', + ); + } +} + +export function listMagazzinoPerOrg(orgId: string): Promise { + return magazzinoRepository.findAllByOrg(orgId).then((voci) => voci.map(toView)); +} + +export interface AggiungiVoceInput { + materialeId: string; + quantitaPosseduta: number; + stato: StatoMagazzinoVoce; + posizione?: string; + note?: string; +} + +export async function aggiungiVoce(orgId: string, input: AggiungiVoceInput): Promise { + await assicuraMaterialeApprovato(input.materialeId); + + const data: CreateMagazzinoVoceData = { ...input, orgId }; + const voce = await magazzinoRepository.create(data); + return toView(voce); +} + +async function assicuraVoceDiOrg(id: string, orgId: string): Promise { + const voce = await magazzinoRepository.findByIdAndOrg(id, orgId); + if (!voce) { + throw new HttpError(404, 'Voce di magazzino non trovata'); + } +} + +export interface AggiornaVoceInput { + materialeId?: string; + quantitaPosseduta?: number; + stato?: StatoMagazzinoVoce; + posizione?: string | null; + note?: string | null; +} + +export async function aggiornaVoce(id: string, orgId: string, input: AggiornaVoceInput): Promise { + await assicuraVoceDiOrg(id, orgId); + + if (input.materialeId !== undefined) { + await assicuraMaterialeApprovato(input.materialeId); + } + + const data: UpdateMagazzinoVoceData = input; + try { + const voce = await magazzinoRepository.update(id, data); + return toView(voce); + } catch (err) { + throw toHttpError(err, 'Voce di magazzino non trovata', 'Il materiale indicato non esiste'); + } +} + +export async function eliminaVoce(id: string, orgId: string): Promise { + await assicuraVoceDiOrg(id, orgId); + try { + await magazzinoRepository.delete(id); + } catch (err) { + throw toHttpError(err, 'Voce di magazzino non trovata', 'Impossibile eliminare la voce di magazzino'); + } +} diff --git a/scouthub-magazzino-be/src/services/materiali.service.ts b/scouthub-magazzino-be/src/services/materiali.service.ts new file mode 100644 index 0000000..e874bed --- /dev/null +++ b/scouthub-magazzino-be/src/services/materiali.service.ts @@ -0,0 +1,84 @@ +import { Materiale, StatoMateriale } from '@prisma/client'; +import { materialiRepository } from '../repositories/materiali.repository'; +import { HttpError } from '../errors'; + +export interface MaterialePubblico { + id: string; + nome: string; + categoria: string; + unitaMisura: string; +} + +export interface MaterialeProposta { + id: string; + nome: string; + categoria: string; + unitaMisura: string; + stato: StatoMateriale; + propostoDaOrgId: string; + creatoIl: Date; +} + +function toPubblico(materiale: Materiale): MaterialePubblico { + return { + id: materiale.id, + nome: materiale.nome, + categoria: materiale.categoria, + unitaMisura: materiale.unitaMisura, + }; +} + +function toProposta(materiale: Materiale): MaterialeProposta { + return { + id: materiale.id, + nome: materiale.nome, + categoria: materiale.categoria, + unitaMisura: materiale.unitaMisura, + stato: materiale.stato, + propostoDaOrgId: materiale.propostoDaOrgId, + creatoIl: materiale.creatoIl, + }; +} + +export async function listMaterialiApprovati(categoria?: string): Promise { + const materiali = await materialiRepository.findApprovati(categoria); + return materiali.map(toPubblico); +} + +export interface ProponiMaterialeInput { + nome: string; + categoria: string; + unitaMisura: string; + orgId: string; +} + +export async function proponiMateriale(input: ProponiMaterialeInput): Promise { + const materiale = await materialiRepository.create({ + nome: input.nome, + categoria: input.categoria, + unitaMisura: input.unitaMisura, + propostoDaOrgId: input.orgId, + }); + return toProposta(materiale); +} + +export async function listProposte(): Promise { + const materiali = await materialiRepository.findProposte(); + return materiali.map(toProposta); +} + +export type DecisioneProposta = 'approvato' | 'rifiutato'; + +export async function decidiProposta(id: string, decisione: DecisioneProposta): Promise { + const materiale = await materialiRepository.findById(id); + if (!materiale) { + throw new HttpError(404, 'Proposta non trovata'); + } + if (materiale.stato !== StatoMateriale.proposto) { + throw new HttpError(409, 'La proposta è già stata decisa'); + } + + const stato = decisione === 'approvato' ? StatoMateriale.approvato : StatoMateriale.rifiutato; + const aggiornato = await materialiRepository.updateStato(id, stato); + return toProposta(aggiornato); +} diff --git a/scouthub-magazzino-be/src/services/tipiEvento.service.ts b/scouthub-magazzino-be/src/services/tipiEvento.service.ts new file mode 100644 index 0000000..dc80a19 --- /dev/null +++ b/scouthub-magazzino-be/src/services/tipiEvento.service.ts @@ -0,0 +1,31 @@ +import { TipoEvento } from '@prisma/client'; +import { tipiEventoRepository } from '../repositories/tipiEvento.repository'; +import { toHttpError } from '../utils/prisma-errors'; + +export function listTipiEvento(): Promise { + return tipiEventoRepository.findAll(); +} + +export function creaTipoEvento(nome: string): Promise { + return tipiEventoRepository.create(nome); +} + +export async function aggiornaTipoEvento(id: string, nome: string): Promise { + try { + return await tipiEventoRepository.update(id, nome); + } catch (err) { + throw toHttpError(err, 'Tipo evento non trovato', 'Conflitto durante l\'aggiornamento del tipo evento'); + } +} + +export async function eliminaTipoEvento(id: string): Promise { + try { + await tipiEventoRepository.delete(id); + } catch (err) { + throw toHttpError( + err, + 'Tipo evento non trovato', + 'Impossibile eliminare il tipo evento: è referenziato da almeno una lista modello', + ); + } +} diff --git a/scouthub-magazzino-be/src/types/express.d.ts b/scouthub-magazzino-be/src/types/express.d.ts new file mode 100644 index 0000000..e9918dd --- /dev/null +++ b/scouthub-magazzino-be/src/types/express.d.ts @@ -0,0 +1,11 @@ +import { AuthContext } from '../auth/auth.types'; + +declare global { + namespace Express { + interface Request { + auth?: AuthContext; + } + } +} + +export {}; diff --git a/scouthub-magazzino-be/src/utils/prisma-errors.ts b/scouthub-magazzino-be/src/utils/prisma-errors.ts new file mode 100644 index 0000000..bc4192e --- /dev/null +++ b/scouthub-magazzino-be/src/utils/prisma-errors.ts @@ -0,0 +1,16 @@ +import { Prisma } from '@prisma/client'; +import { HttpError } from '../errors'; + +// Converte gli errori noti di Prisma (vincoli FK, record non trovato) in HttpError +// con uno status code sensato, senza dover ripetere lo stesso catch ovunque. +export function toHttpError(err: unknown, notFoundMessage: string, conflictMessage: string): unknown { + if (err instanceof Prisma.PrismaClientKnownRequestError) { + if (err.code === 'P2025') { + return new HttpError(404, notFoundMessage); + } + if (err.code === 'P2003') { + return new HttpError(409, conflictMessage); + } + } + return err; +} diff --git a/scouthub-magazzino-be/tests/integration/eventi.endpoint.test.ts b/scouthub-magazzino-be/tests/integration/eventi.endpoint.test.ts new file mode 100644 index 0000000..9b9ece2 --- /dev/null +++ b/scouthub-magazzino-be/tests/integration/eventi.endpoint.test.ts @@ -0,0 +1,254 @@ +import { generateKeyPairSync } from 'crypto'; +import request from 'supertest'; +import nock from 'nock'; +import jwt from 'jsonwebtoken'; + +process.env.KEYCLOAK_BASE_URL = 'http://keycloak.test'; +process.env.KEYCLOAK_REALM = 'scouthub'; +process.env.KEYCLOAK_MAGAZZINO_CLIENT_ID = 'test-client'; +process.env.KEYCLOAK_MAGAZZINO_CLIENT_SECRET = 'test-secret'; +process.env.DATABASE_URL = 'postgresql://user:pass@localhost:5432/scouthub_magazzino_test'; + +const eventoFindFirst = jest.fn(); +const eventoCreate = jest.fn(); +const eventoCheckUpsert = jest.fn(); +const listaFindFirst = jest.fn(); +const magazzinoVoceFindMany = jest.fn(); + +jest.mock('../../src/db/prisma', () => ({ + prisma: { + evento: { + findFirst: (...args: unknown[]) => eventoFindFirst(...args), + create: (...args: unknown[]) => eventoCreate(...args), + }, + eventoCheck: { + upsert: (...args: unknown[]) => eventoCheckUpsert(...args), + }, + lista: { + findFirst: (...args: unknown[]) => listaFindFirst(...args), + }, + magazzinoVoce: { + findMany: (...args: unknown[]) => magazzinoVoceFindMany(...args), + }, + }, +})); + +import { app } from '../../src/app'; + +const KEYCLOAK_HOST = 'http://keycloak.test'; +const CERTS_PATH = '/realms/scouthub/protocol/openid-connect/certs'; +const KID = 'test-kid'; + +const { publicKey, privateKey } = generateKeyPairSync('rsa', { modulusLength: 2048 }); +const jwk = publicKey.export({ format: 'jwk' }) as Record; +const privateKeyPem = privateKey.export({ type: 'pkcs1', format: 'pem' }) as string; + +function signToken(payload: object): string { + return jwt.sign(payload, privateKeyPem, { algorithm: 'RS256', keyid: KID, expiresIn: '5m' }); +} + +function tokenOrg(orgId: string): string { + return signToken({ + sub: 'user-1', + realm_access: { roles: ['censito'] }, + organization: { gruppo: { id: orgId, roles: [] } }, + }); +} + +function materiale(id: string, nome: string, unitaMisura: string) { + return { id, nome, categoria: 'x', unitaMisura, stato: 'approvato', propostoDaOrgId: 'org-seed', creatoIl: new Date() }; +} + +// Evento con lista a due voci (Tenda x2, Torcia x4): un materiale è tracciato +// in magazzino, l'altro no (deve risultare quantitaPosseduta: 0 di default). +function eventoConDettagli(overrides: Partial> = {}) { + return { + id: 'ev-1', + orgId: 'org-a', + nome: 'Campo estivo 2026', + listaId: 'l-1', + data: new Date('2026-08-01'), + lista: { + id: 'l-1', + nome: 'Kit campo estivo', + orgId: 'org-a', + creataIl: new Date('2026-01-01'), + voci: [ + { listaId: 'l-1', materialeId: 'm-1', quantita: 2, materiale: materiale('m-1', 'Tenda', 'pz') }, + { listaId: 'l-1', materialeId: 'm-2', quantita: 4, materiale: materiale('m-2', 'Torcia', 'pz') }, + ], + }, + check: [{ eventoId: 'ev-1', materialeId: 'm-1', portato: true, note: 'controllata' }], + ...overrides, + }; +} + +beforeAll(() => { + nock(KEYCLOAK_HOST).persist().get(CERTS_PATH).reply(200, { + keys: [{ ...jwk, kid: KID, alg: 'RS256', use: 'sig' }], + }); +}); + +afterAll(() => { + nock.cleanAll(); +}); + +beforeEach(() => { + jest.clearAllMocks(); +}); + +describe('POST /eventi', () => { + test("crea l'evento per l'org corrente se la lista appartiene alla stessa org", async () => { + listaFindFirst.mockResolvedValueOnce({ id: 'l-1', nome: 'Kit', orgId: 'org-a', creataIl: new Date(), voci: [] }); + eventoCreate.mockResolvedValueOnce(eventoConDettagli({ check: [] })); + magazzinoVoceFindMany.mockResolvedValueOnce([]); + + const response = await request(app) + .post('/eventi') + .set('Authorization', `Bearer ${tokenOrg('org-a')}`) + .send({ nome: 'Campo estivo 2026', listaId: 'l-1', data: '2026-08-01' }); + + expect(response.status).toBe(201); + expect(listaFindFirst).toHaveBeenCalledWith( + expect.objectContaining({ where: { id: 'l-1', orgId: 'org-a' } }), + ); + expect(eventoCreate).toHaveBeenCalledWith( + expect.objectContaining({ data: expect.objectContaining({ orgId: 'org-a', listaId: 'l-1' }) }), + ); + }); + + test('risponde 400 se la lista non esiste o appartiene a un\'altra org', async () => { + listaFindFirst.mockResolvedValueOnce(null); + + const response = await request(app) + .post('/eventi') + .set('Authorization', `Bearer ${tokenOrg('org-a')}`) + .send({ nome: 'Campo estivo 2026', listaId: 'l-di-unaltra-org', data: '2026-08-01' }); + + expect(response.status).toBe(400); + expect(eventoCreate).not.toHaveBeenCalled(); + }); + + test('risponde 401 senza token', async () => { + const response = await request(app).post('/eventi').send({ nome: 'x', listaId: 'l-1', data: '2026-08-01' }); + + expect(response.status).toBe(401); + expect(eventoCreate).not.toHaveBeenCalled(); + }); +}); + +describe('GET /eventi/:id — join lista <-> magazzino', () => { + test("un'org non può leggere un evento di un'altra org", async () => { + eventoFindFirst.mockResolvedValueOnce(null); + + const response = await request(app).get('/eventi/ev-1').set('Authorization', `Bearer ${tokenOrg('org-b')}`); + + expect(response.status).toBe(404); + expect(eventoFindFirst).toHaveBeenCalledWith( + expect.objectContaining({ where: { id: 'ev-1', orgId: 'org-b' } }), + ); + expect(magazzinoVoceFindMany).not.toHaveBeenCalled(); + }); + + test('combina, per ogni voce della lista, quantità posseduta in magazzino e stato di check', async () => { + eventoFindFirst.mockResolvedValueOnce(eventoConDettagli()); + // Solo m-1 è tracciato in magazzino (5 posseduti); m-2 non ha alcuna riga. + magazzinoVoceFindMany.mockResolvedValueOnce([{ materialeId: 'm-1', quantitaPosseduta: 5 }]); + + const response = await request(app).get('/eventi/ev-1').set('Authorization', `Bearer ${tokenOrg('org-a')}`); + + expect(response.status).toBe(200); + expect(magazzinoVoceFindMany).toHaveBeenCalledWith({ + where: { orgId: 'org-a', materialeId: { in: ['m-1', 'm-2'] } }, + select: { materialeId: true, quantitaPosseduta: true }, + }); + expect(response.body).toEqual({ + id: 'ev-1', + orgId: 'org-a', + nome: 'Campo estivo 2026', + listaId: 'l-1', + data: '2026-08-01T00:00:00.000Z', + voci: [ + { + materialeId: 'm-1', + nome: 'Tenda', + unitaMisura: 'pz', + quantitaRichiesta: 2, + quantitaPosseduta: 5, + portato: true, + note: 'controllata', + }, + { + materialeId: 'm-2', + nome: 'Torcia', + unitaMisura: 'pz', + quantitaRichiesta: 4, + quantitaPosseduta: 0, + portato: false, + note: null, + }, + ], + }); + }); + + test('risponde 401 senza token', async () => { + const response = await request(app).get('/eventi/ev-1'); + + expect(response.status).toBe(401); + expect(eventoFindFirst).not.toHaveBeenCalled(); + }); +}); + +describe('PATCH /eventi/:id/check', () => { + test('aggiorna portato/note per una voce e restituisce il dettaglio aggiornato', async () => { + eventoFindFirst + .mockResolvedValueOnce(eventoConDettagli({ check: [] })) // ownership check dentro aggiornaCheckEvento + .mockResolvedValueOnce(eventoConDettagli()); // rilettura per la response + eventoCheckUpsert.mockResolvedValueOnce({ eventoId: 'ev-1', materialeId: 'm-1', portato: true, note: 'controllata' }); + magazzinoVoceFindMany.mockResolvedValue([{ materialeId: 'm-1', quantitaPosseduta: 5 }]); + + const response = await request(app) + .patch('/eventi/ev-1/check') + .set('Authorization', `Bearer ${tokenOrg('org-a')}`) + .send({ voci: [{ materialeId: 'm-1', portato: true, note: 'controllata' }] }); + + expect(response.status).toBe(200); + expect(eventoCheckUpsert).toHaveBeenCalledWith({ + where: { eventoId_materialeId: { eventoId: 'ev-1', materialeId: 'm-1' } }, + create: { eventoId: 'ev-1', materialeId: 'm-1', portato: true, note: 'controllata' }, + update: { portato: true, note: 'controllata' }, + }); + expect(response.body.voci[0]).toMatchObject({ materialeId: 'm-1', portato: true, note: 'controllata' }); + }); + + test('rifiuta un materialeId che non appartiene alla lista collegata (400)', async () => { + eventoFindFirst.mockResolvedValueOnce(eventoConDettagli()); + + const response = await request(app) + .patch('/eventi/ev-1/check') + .set('Authorization', `Bearer ${tokenOrg('org-a')}`) + .send({ voci: [{ materialeId: 'm-estraneo', portato: true }] }); + + expect(response.status).toBe(400); + expect(eventoCheckUpsert).not.toHaveBeenCalled(); + }); + + test("un'org non può aggiornare il check di un evento di un'altra org", async () => { + eventoFindFirst.mockResolvedValueOnce(null); + + const response = await request(app) + .patch('/eventi/ev-1/check') + .set('Authorization', `Bearer ${tokenOrg('org-b')}`) + .send({ voci: [{ materialeId: 'm-1', portato: true }] }); + + expect(response.status).toBe(404); + expect(eventoCheckUpsert).not.toHaveBeenCalled(); + }); + + test('risponde 401 senza token', async () => { + const response = await request(app).patch('/eventi/ev-1/check').send({ voci: [{ materialeId: 'm-1', portato: true }] }); + + expect(response.status).toBe(401); + expect(eventoCheckUpsert).not.toHaveBeenCalled(); + }); +}); diff --git a/scouthub-magazzino-be/tests/integration/liste.endpoint.test.ts b/scouthub-magazzino-be/tests/integration/liste.endpoint.test.ts new file mode 100644 index 0000000..bc2e46d --- /dev/null +++ b/scouthub-magazzino-be/tests/integration/liste.endpoint.test.ts @@ -0,0 +1,314 @@ +import { generateKeyPairSync } from 'crypto'; +import request from 'supertest'; +import nock from 'nock'; +import jwt from 'jsonwebtoken'; + +process.env.KEYCLOAK_BASE_URL = 'http://keycloak.test'; +process.env.KEYCLOAK_REALM = 'scouthub'; +process.env.KEYCLOAK_MAGAZZINO_CLIENT_ID = 'test-client'; +process.env.KEYCLOAK_MAGAZZINO_CLIENT_SECRET = 'test-secret'; +process.env.DATABASE_URL = 'postgresql://user:pass@localhost:5432/scouthub_magazzino_test'; + +const listaFindMany = jest.fn(); +const listaFindFirst = jest.fn(); +const listaCreate = jest.fn(); +const listaUpdate = jest.fn(); +const listaDelete = jest.fn(); +const listaVoceDeleteMany = jest.fn(); + +const listaModelloFindUnique = jest.fn(); +const listaModelloUpdate = jest.fn(); +const listaModelloVoceDeleteMany = jest.fn(); + +// $transaction condiviso da entrambi i repository (liste e liste-modello): il tx +// espone gli stessi metodi mockati usati fuori transazione, così i test possono +// asserire su un'unica lista di chiamate indipendentemente dal fatto che passino +// per una transazione o meno. +const transactionFn = jest.fn(async (callback: (tx: unknown) => unknown) => + callback({ + lista: { update: listaUpdate, delete: listaDelete }, + listaVoce: { deleteMany: listaVoceDeleteMany }, + listaModello: { update: listaModelloUpdate }, + listaModelloVoce: { deleteMany: listaModelloVoceDeleteMany }, + }), +); + +jest.mock('../../src/db/prisma', () => ({ + prisma: { + lista: { + findMany: (...args: unknown[]) => listaFindMany(...args), + findFirst: (...args: unknown[]) => listaFindFirst(...args), + create: (...args: unknown[]) => listaCreate(...args), + }, + listaVoce: { + deleteMany: (...args: unknown[]) => listaVoceDeleteMany(...args), + }, + listaModello: { + findUnique: (...args: unknown[]) => listaModelloFindUnique(...args), + update: (...args: unknown[]) => listaModelloUpdate(...args), + }, + listaModelloVoce: { + deleteMany: (...args: unknown[]) => listaModelloVoceDeleteMany(...args), + }, + $transaction: (...args: unknown[]) => transactionFn(...(args as [(tx: unknown) => unknown])), + }, +})); + +import { app } from '../../src/app'; + +const KEYCLOAK_HOST = 'http://keycloak.test'; +const CERTS_PATH = '/realms/scouthub/protocol/openid-connect/certs'; +const KID = 'test-kid'; + +const { publicKey, privateKey } = generateKeyPairSync('rsa', { modulusLength: 2048 }); +const jwk = publicKey.export({ format: 'jwk' }) as Record; +const privateKeyPem = privateKey.export({ type: 'pkcs1', format: 'pem' }) as string; + +function signToken(payload: object): string { + return jwt.sign(payload, privateKeyPem, { algorithm: 'RS256', keyid: KID, expiresIn: '5m' }); +} + +function tokenOrg(orgId: string): string { + return signToken({ + sub: 'user-1', + realm_access: { roles: ['censito'] }, + organization: { gruppo: { id: orgId, roles: [] } }, + }); +} + +function adminCatalogoToken(): string { + return signToken({ + sub: 'admin-1', + realm_access: { roles: ['moderatore'] }, + organization: { 'gruppo-omega': { id: 'org-9', roles: [] } }, + }); +} + +function materialeJoin(id: string, nome: string, unitaMisura: string) { + return { id, nome, categoria: 'x', unitaMisura, stato: 'approvato', propostoDaOrgId: 'org-seed', creatoIl: new Date() }; +} + +beforeAll(() => { + nock(KEYCLOAK_HOST).persist().get(CERTS_PATH).reply(200, { + keys: [{ ...jwk, kid: KID, alg: 'RS256', use: 'sig' }], + }); +}); + +afterAll(() => { + nock.cleanAll(); +}); + +beforeEach(() => { + jest.clearAllMocks(); +}); + +describe('GET /liste', () => { + test('restituisce solo le liste dell\'org corrente, ricavata dal token', async () => { + listaFindMany.mockResolvedValueOnce([]); + + await request(app).get('/liste').set('Authorization', `Bearer ${tokenOrg('org-a')}`); + + expect(listaFindMany).toHaveBeenCalledWith( + expect.objectContaining({ where: { orgId: 'org-a' } }), + ); + }); + + test('org diverse ottengono query filtrate su org_id diversi', async () => { + listaFindMany.mockResolvedValue([]); + + await request(app).get('/liste').set('Authorization', `Bearer ${tokenOrg('org-a')}`); + await request(app).get('/liste').set('Authorization', `Bearer ${tokenOrg('org-b')}`); + + expect(listaFindMany).toHaveBeenNthCalledWith(1, expect.objectContaining({ where: { orgId: 'org-a' } })); + expect(listaFindMany).toHaveBeenNthCalledWith(2, expect.objectContaining({ where: { orgId: 'org-b' } })); + }); + + test('risponde 401 senza token', async () => { + const response = await request(app).get('/liste'); + + expect(response.status).toBe(401); + expect(listaFindMany).not.toHaveBeenCalled(); + }); +}); + +describe('POST /liste', () => { + test('crea una lista vuota per l\'org corrente, ignorando un org_id eventualmente inviato dal client', async () => { + listaCreate.mockResolvedValueOnce({ id: 'l-1', nome: 'Lista vuota', orgId: 'org-a', creataIl: new Date(), voci: [] }); + + const response = await request(app) + .post('/liste') + .set('Authorization', `Bearer ${tokenOrg('org-a')}`) + .send({ nome: 'Lista vuota', orgId: 'org-spoofed' }); + + expect(response.status).toBe(201); + expect(listaCreate).toHaveBeenCalledWith({ + data: { nome: 'Lista vuota', orgId: 'org-a', voci: { create: [] } }, + include: { voci: { include: { materiale: true } } }, + }); + }); +}); + +describe('POST /liste/da-modello/:listaModelloId', () => { + test('copia nome e voci dalla lista modello, senza salvare alcun riferimento verso di essa', async () => { + const vociModello = [{ materialeId: 'm-1', quantita: 2, materiale: materialeJoin('m-1', 'Tenda', 'pz') }]; + listaModelloFindUnique.mockResolvedValueOnce({ + id: 'lm-1', + nome: 'Kit campo estivo', + tipoEventoId: 't-1', + pubblica: true, + voci: vociModello, + }); + listaCreate.mockResolvedValueOnce({ + id: 'l-2', + nome: 'Kit campo estivo', + orgId: 'org-a', + creataIl: new Date(), + voci: [{ materialeId: 'm-1', quantita: 2, materiale: materialeJoin('m-1', 'Tenda', 'pz') }], + }); + + const response = await request(app) + .post('/liste/da-modello/lm-1') + .set('Authorization', `Bearer ${tokenOrg('org-a')}`); + + expect(response.status).toBe(201); + expect(JSON.stringify(response.body)).not.toMatch(/listaModello/i); + + const callArg = listaCreate.mock.calls[0][0]; + expect(callArg.data).toEqual({ + nome: 'Kit campo estivo', + orgId: 'org-a', + voci: { create: [{ materialeId: 'm-1', quantita: 2 }] }, + }); + // Le voci passate a create sono un array nuovo con valori copiati, non lo + // stesso array (né gli stessi oggetti) restituiti dalla lista modello. + expect(callArg.data.voci.create).not.toBe(vociModello); + }); + + test('risponde 404 se la lista modello non esiste', async () => { + listaModelloFindUnique.mockResolvedValueOnce(null); + + const response = await request(app) + .post('/liste/da-modello/inesistente') + .set('Authorization', `Bearer ${tokenOrg('org-a')}`); + + expect(response.status).toBe(404); + expect(listaCreate).not.toHaveBeenCalled(); + }); + + test('risponde 401 senza token', async () => { + const response = await request(app).post('/liste/da-modello/lm-1'); + + expect(response.status).toBe(401); + expect(listaCreate).not.toHaveBeenCalled(); + }); +}); + +describe('PUT /liste/:id — isolamento tra org', () => { + test('un\'org non può modificare una lista di un\'altra org (risponde 404, non 403, per non rivelarne l\'esistenza)', async () => { + // La query combina sempre id + orgId: una lista di un'altra org non viene trovata. + listaFindFirst.mockResolvedValueOnce(null); + + const response = await request(app) + .put('/liste/l-1') + .set('Authorization', `Bearer ${tokenOrg('org-b')}`) + .send({ nome: 'Nome modificato' }); + + expect(response.status).toBe(404); + expect(listaFindFirst).toHaveBeenCalledWith( + expect.objectContaining({ where: { id: 'l-1', orgId: 'org-b' } }), + ); + expect(listaUpdate).not.toHaveBeenCalled(); + }); + + test('l\'org proprietaria può modificare la propria lista', async () => { + listaFindFirst.mockResolvedValueOnce({ id: 'l-1', nome: 'Vecchio nome', orgId: 'org-a', creataIl: new Date(), voci: [] }); + listaUpdate.mockResolvedValueOnce({ id: 'l-1', nome: 'Nuovo nome', orgId: 'org-a', creataIl: new Date(), voci: [] }); + + const response = await request(app) + .put('/liste/l-1') + .set('Authorization', `Bearer ${tokenOrg('org-a')}`) + .send({ nome: 'Nuovo nome' }); + + expect(response.status).toBe(200); + expect(listaUpdate).toHaveBeenCalledWith( + expect.objectContaining({ where: { id: 'l-1' }, data: expect.objectContaining({ nome: 'Nuovo nome' }) }), + ); + }); + + test('risponde 401 senza token', async () => { + const response = await request(app).put('/liste/l-1').send({ nome: 'x' }); + + expect(response.status).toBe(401); + expect(listaFindFirst).not.toHaveBeenCalled(); + }); +}); + +describe('DELETE /liste/:id — isolamento tra org', () => { + test('un\'org non può eliminare una lista di un\'altra org', async () => { + listaFindFirst.mockResolvedValueOnce(null); + + const response = await request(app).delete('/liste/l-1').set('Authorization', `Bearer ${tokenOrg('org-b')}`); + + expect(response.status).toBe(404); + expect(listaDelete).not.toHaveBeenCalled(); + }); + + test('l\'org proprietaria può eliminare la propria lista', async () => { + listaFindFirst.mockResolvedValueOnce({ id: 'l-1', nome: 'Lista', orgId: 'org-a', creataIl: new Date(), voci: [] }); + listaDelete.mockResolvedValueOnce({ id: 'l-1' }); + + const response = await request(app).delete('/liste/l-1').set('Authorization', `Bearer ${tokenOrg('org-a')}`); + + expect(response.status).toBe(204); + expect(listaVoceDeleteMany).toHaveBeenCalledWith({ where: { listaId: 'l-1' } }); + expect(listaDelete).toHaveBeenCalledWith({ where: { id: 'l-1' } }); + }); +}); + +describe('indipendenza tra lista modello e lista forkata', () => { + test('aggiornare la lista modello originale non tocca in alcun modo le tabelle della lista privata forkata', async () => { + listaModelloUpdate.mockResolvedValueOnce({ + id: 'lm-1', + nome: 'Kit aggiornato', + tipoEventoId: 't-1', + pubblica: true, + voci: [], + }); + + const response = await request(app) + .put('/liste-modello/lm-1') + .set('Authorization', `Bearer ${adminCatalogoToken()}`) + .send({ nome: 'Kit aggiornato', voci: [] }); + + expect(response.status).toBe(200); + expect(listaModelloVoceDeleteMany).toHaveBeenCalledWith({ where: { listaModelloId: 'lm-1' } }); + expect(listaModelloUpdate).toHaveBeenCalled(); + // Nessuna chiamata sulle tabelle delle liste private: le due entità sono + // completamente disgiunte dopo il fork. + expect(listaVoceDeleteMany).not.toHaveBeenCalled(); + expect(listaUpdate).not.toHaveBeenCalled(); + expect(listaDelete).not.toHaveBeenCalled(); + }); + + test('aggiornare la lista privata forkata non tocca in alcun modo le tabelle della lista modello originale', async () => { + listaFindFirst.mockResolvedValueOnce({ id: 'l-2', nome: 'Kit campo estivo', orgId: 'org-a', creataIl: new Date(), voci: [] }); + listaUpdate.mockResolvedValueOnce({ + id: 'l-2', + nome: 'Kit campo estivo (personalizzato)', + orgId: 'org-a', + creataIl: new Date(), + voci: [], + }); + + const response = await request(app) + .put('/liste/l-2') + .set('Authorization', `Bearer ${tokenOrg('org-a')}`) + .send({ nome: 'Kit campo estivo (personalizzato)', voci: [] }); + + expect(response.status).toBe(200); + expect(listaVoceDeleteMany).toHaveBeenCalledWith({ where: { listaId: 'l-2' } }); + expect(listaUpdate).toHaveBeenCalled(); + expect(listaModelloVoceDeleteMany).not.toHaveBeenCalled(); + expect(listaModelloUpdate).not.toHaveBeenCalled(); + }); +}); diff --git a/scouthub-magazzino-be/tests/integration/listeModello.endpoint.test.ts b/scouthub-magazzino-be/tests/integration/listeModello.endpoint.test.ts new file mode 100644 index 0000000..1bde0d3 --- /dev/null +++ b/scouthub-magazzino-be/tests/integration/listeModello.endpoint.test.ts @@ -0,0 +1,252 @@ +import { generateKeyPairSync } from 'crypto'; +import request from 'supertest'; +import nock from 'nock'; +import jwt from 'jsonwebtoken'; + +process.env.KEYCLOAK_BASE_URL = 'http://keycloak.test'; +process.env.KEYCLOAK_REALM = 'scouthub'; +process.env.KEYCLOAK_MAGAZZINO_CLIENT_ID = 'test-client'; +process.env.KEYCLOAK_MAGAZZINO_CLIENT_SECRET = 'test-secret'; +process.env.DATABASE_URL = 'postgresql://user:pass@localhost:5432/scouthub_magazzino_test'; + +const listaModelloFindMany = jest.fn(); +const listaModelloCreate = jest.fn(); + +// tx espone gli stessi metodi usati dal repository dentro $transaction; i test su +// update/delete forniscono un tx dedicato via mockImplementationOnce. +const listaModelloVoceDeleteMany = jest.fn(); +const listaModelloUpdate = jest.fn(); +const listaModelloDelete = jest.fn(); + +const transactionFn = jest.fn(async (callback: (tx: unknown) => unknown) => + callback({ + listaModelloVoce: { deleteMany: listaModelloVoceDeleteMany }, + listaModello: { update: listaModelloUpdate, delete: listaModelloDelete }, + }), +); + +jest.mock('../../src/db/prisma', () => ({ + prisma: { + listaModello: { + findMany: (...args: unknown[]) => listaModelloFindMany(...args), + create: (...args: unknown[]) => listaModelloCreate(...args), + }, + $transaction: (...args: unknown[]) => transactionFn(...(args as [(tx: unknown) => unknown])), + }, +})); + +import { app } from '../../src/app'; + +const KEYCLOAK_HOST = 'http://keycloak.test'; +const CERTS_PATH = '/realms/scouthub/protocol/openid-connect/certs'; +const KID = 'test-kid'; + +const { publicKey, privateKey } = generateKeyPairSync('rsa', { modulusLength: 2048 }); +const jwk = publicKey.export({ format: 'jwk' }) as Record; +const privateKeyPem = privateKey.export({ type: 'pkcs1', format: 'pem' }) as string; + +function signToken(payload: object): string { + return jwt.sign(payload, privateKeyPem, { algorithm: 'RS256', keyid: KID, expiresIn: '5m' }); +} + +function utenteToken(): string { + return signToken({ + sub: 'user-1', + realm_access: { roles: ['censito'] }, + organization: { 'gruppo-alfa': { id: 'org-1', roles: [] } }, + }); +} + +function adminCatalogoToken(): string { + return signToken({ + sub: 'admin-1', + realm_access: { roles: ['moderatore'] }, + organization: { 'gruppo-omega': { id: 'org-9', roles: [] } }, + }); +} + +beforeAll(() => { + nock(KEYCLOAK_HOST).persist().get(CERTS_PATH).reply(200, { + keys: [{ ...jwk, kid: KID, alg: 'RS256', use: 'sig' }], + }); +}); + +afterAll(() => { + nock.cleanAll(); +}); + +beforeEach(() => { + jest.clearAllMocks(); +}); + +describe('GET /liste-modello', () => { + test('è pubblico e restituisce le liste con le voci (materiale + quantità)', async () => { + listaModelloFindMany.mockResolvedValueOnce([ + { + id: 'lm-1', + nome: 'Kit campo estivo', + tipoEventoId: 't-1', + pubblica: true, + voci: [{ materialeId: 'm-1', quantita: 2, materiale: { id: 'm-1', nome: 'Tenda', unitaMisura: 'pz' } }], + }, + ]); + + const response = await request(app).get('/liste-modello'); + + expect(response.status).toBe(200); + expect(response.body).toEqual([ + { + id: 'lm-1', + nome: 'Kit campo estivo', + tipoEventoId: 't-1', + voci: [{ materialeId: 'm-1', nome: 'Tenda', unitaMisura: 'pz', quantita: 2 }], + }, + ]); + expect(listaModelloFindMany).toHaveBeenCalledWith( + expect.objectContaining({ where: { pubblica: true } }), + ); + }); + + test('filtra per tipoEventoId quando richiesto', async () => { + listaModelloFindMany.mockResolvedValueOnce([]); + + await request(app).get('/liste-modello').query({ tipoEventoId: 't-1' }); + + expect(listaModelloFindMany).toHaveBeenCalledWith( + expect.objectContaining({ where: { pubblica: true, tipoEventoId: 't-1' } }), + ); + }); +}); + +describe('POST /liste-modello', () => { + const body = { nome: 'Kit bivacco', tipoEventoId: 't-2', voci: [{ materialeId: 'm-1', quantita: 3 }] }; + + test('un utente normale non può creare una lista modello', async () => { + const response = await request(app) + .post('/liste-modello') + .set('Authorization', `Bearer ${utenteToken()}`) + .send(body); + + expect(response.status).toBe(403); + expect(listaModelloCreate).not.toHaveBeenCalled(); + }); + + test('un moderatore può creare una lista modello, sempre pubblica', async () => { + listaModelloCreate.mockResolvedValueOnce({ + id: 'lm-2', + nome: 'Kit bivacco', + tipoEventoId: 't-2', + pubblica: true, + voci: [{ materialeId: 'm-1', quantita: 3, materiale: { id: 'm-1', nome: 'Corda', unitaMisura: 'm' } }], + }); + + const response = await request(app) + .post('/liste-modello') + .set('Authorization', `Bearer ${adminCatalogoToken()}`) + .send(body); + + expect(response.status).toBe(201); + expect(listaModelloCreate).toHaveBeenCalledWith({ + data: { + nome: 'Kit bivacco', + tipoEventoId: 't-2', + pubblica: true, + voci: { create: [{ materialeId: 'm-1', quantita: 3 }] }, + }, + include: { voci: { include: { materiale: true } } }, + }); + }); + + test('ignora un eventuale pubblica:false inviato dal client, resta sempre true', async () => { + listaModelloCreate.mockResolvedValueOnce({ + id: 'lm-3', + nome: 'Kit bivacco', + tipoEventoId: 't-2', + pubblica: true, + voci: [], + }); + + await request(app) + .post('/liste-modello') + .set('Authorization', `Bearer ${adminCatalogoToken()}`) + .send({ ...body, voci: [], pubblica: false }); + + expect(listaModelloCreate).toHaveBeenCalledWith( + expect.objectContaining({ data: expect.objectContaining({ pubblica: true }) }), + ); + }); + + test('risponde 401 senza token', async () => { + const response = await request(app).post('/liste-modello').send(body); + + expect(response.status).toBe(401); + expect(listaModelloCreate).not.toHaveBeenCalled(); + }); +}); + +describe('PUT /liste-modello/:id', () => { + test('un utente normale non può modificare una lista modello', async () => { + const response = await request(app) + .put('/liste-modello/lm-1') + .set('Authorization', `Bearer ${utenteToken()}`) + .send({ nome: 'Kit aggiornato' }); + + expect(response.status).toBe(403); + expect(listaModelloUpdate).not.toHaveBeenCalled(); + }); + + test('un moderatore può modificare nome e voci di una lista modello', async () => { + listaModelloUpdate.mockResolvedValueOnce({ + id: 'lm-1', + nome: 'Kit aggiornato', + tipoEventoId: 't-1', + pubblica: true, + voci: [{ materialeId: 'm-2', quantita: 1, materiale: { id: 'm-2', nome: 'Torcia', unitaMisura: 'pz' } }], + }); + + const response = await request(app) + .put('/liste-modello/lm-1') + .set('Authorization', `Bearer ${adminCatalogoToken()}`) + .send({ nome: 'Kit aggiornato', voci: [{ materialeId: 'm-2', quantita: 1 }] }); + + expect(response.status).toBe(200); + expect(listaModelloVoceDeleteMany).toHaveBeenCalledWith({ where: { listaModelloId: 'lm-1' } }); + expect(listaModelloUpdate).toHaveBeenCalledWith( + expect.objectContaining({ + where: { id: 'lm-1' }, + data: expect.objectContaining({ + nome: 'Kit aggiornato', + voci: { create: [{ materialeId: 'm-2', quantita: 1 }] }, + }), + }), + ); + }); +}); + +describe('DELETE /liste-modello/:id', () => { + test('un utente normale non può eliminare una lista modello', async () => { + const response = await request(app).delete('/liste-modello/lm-1').set('Authorization', `Bearer ${utenteToken()}`); + + expect(response.status).toBe(403); + expect(listaModelloDelete).not.toHaveBeenCalled(); + }); + + test('un moderatore può eliminare una lista modello (e le sue voci)', async () => { + listaModelloDelete.mockResolvedValueOnce({ id: 'lm-1' }); + + const response = await request(app) + .delete('/liste-modello/lm-1') + .set('Authorization', `Bearer ${adminCatalogoToken()}`); + + expect(response.status).toBe(204); + expect(listaModelloVoceDeleteMany).toHaveBeenCalledWith({ where: { listaModelloId: 'lm-1' } }); + expect(listaModelloDelete).toHaveBeenCalledWith({ where: { id: 'lm-1' } }); + }); + + test('risponde 401 senza token', async () => { + const response = await request(app).delete('/liste-modello/lm-1'); + + expect(response.status).toBe(401); + expect(listaModelloDelete).not.toHaveBeenCalled(); + }); +}); diff --git a/scouthub-magazzino-be/tests/integration/magazzino.endpoint.test.ts b/scouthub-magazzino-be/tests/integration/magazzino.endpoint.test.ts new file mode 100644 index 0000000..ebd107c --- /dev/null +++ b/scouthub-magazzino-be/tests/integration/magazzino.endpoint.test.ts @@ -0,0 +1,304 @@ +import { generateKeyPairSync } from 'crypto'; +import request from 'supertest'; +import nock from 'nock'; +import jwt from 'jsonwebtoken'; + +process.env.KEYCLOAK_BASE_URL = 'http://keycloak.test'; +process.env.KEYCLOAK_REALM = 'scouthub'; +process.env.KEYCLOAK_MAGAZZINO_CLIENT_ID = 'test-client'; +process.env.KEYCLOAK_MAGAZZINO_CLIENT_SECRET = 'test-secret'; +process.env.DATABASE_URL = 'postgresql://user:pass@localhost:5432/scouthub_magazzino_test'; + +const magazzinoVoceFindMany = jest.fn(); +const magazzinoVoceFindFirst = jest.fn(); +const magazzinoVoceCreate = jest.fn(); +const magazzinoVoceUpdate = jest.fn(); +const magazzinoVoceDelete = jest.fn(); +const materialeFindUnique = jest.fn(); + +jest.mock('../../src/db/prisma', () => ({ + prisma: { + magazzinoVoce: { + findMany: (...args: unknown[]) => magazzinoVoceFindMany(...args), + findFirst: (...args: unknown[]) => magazzinoVoceFindFirst(...args), + create: (...args: unknown[]) => magazzinoVoceCreate(...args), + update: (...args: unknown[]) => magazzinoVoceUpdate(...args), + delete: (...args: unknown[]) => magazzinoVoceDelete(...args), + }, + materiale: { + findUnique: (...args: unknown[]) => materialeFindUnique(...args), + }, + }, +})); + +import { app } from '../../src/app'; + +const KEYCLOAK_HOST = 'http://keycloak.test'; +const CERTS_PATH = '/realms/scouthub/protocol/openid-connect/certs'; +const KID = 'test-kid'; + +const { publicKey, privateKey } = generateKeyPairSync('rsa', { modulusLength: 2048 }); +const jwk = publicKey.export({ format: 'jwk' }) as Record; +const privateKeyPem = privateKey.export({ type: 'pkcs1', format: 'pem' }) as string; + +function signToken(payload: object): string { + return jwt.sign(payload, privateKeyPem, { algorithm: 'RS256', keyid: KID, expiresIn: '5m' }); +} + +function tokenOrg(orgId: string): string { + return signToken({ + sub: 'user-1', + realm_access: { roles: ['censito'] }, + organization: { gruppo: { id: orgId, roles: [] } }, + }); +} + +function materialeApprovato(id = 'm-1') { + return { + id, + nome: 'Tenda canadese', + categoria: 'campeggio', + unitaMisura: 'pz', + stato: 'approvato', + propostoDaOrgId: 'org-seed', + creatoIl: new Date(), + }; +} + +function voceConMateriale(overrides: Partial> = {}) { + return { + id: 'mv-1', + orgId: 'org-a', + materialeId: 'm-1', + quantitaPosseduta: 3, + stato: 'buono', + posizione: 'scaffale A', + note: null, + materiale: materialeApprovato(), + ...overrides, + }; +} + +beforeAll(() => { + nock(KEYCLOAK_HOST).persist().get(CERTS_PATH).reply(200, { + keys: [{ ...jwk, kid: KID, alg: 'RS256', use: 'sig' }], + }); +}); + +afterAll(() => { + nock.cleanAll(); +}); + +beforeEach(() => { + jest.clearAllMocks(); +}); + +describe('GET /magazzino', () => { + test('restituisce l\'inventario dell\'org corrente, con nome/categoria del materiale', async () => { + magazzinoVoceFindMany.mockResolvedValueOnce([voceConMateriale()]); + + const response = await request(app).get('/magazzino').set('Authorization', `Bearer ${tokenOrg('org-a')}`); + + expect(response.status).toBe(200); + expect(magazzinoVoceFindMany).toHaveBeenCalledWith( + expect.objectContaining({ where: { orgId: 'org-a' } }), + ); + expect(response.body).toEqual([ + { + id: 'mv-1', + orgId: 'org-a', + materialeId: 'm-1', + materialeNome: 'Tenda canadese', + materialeCategoria: 'campeggio', + quantitaPosseduta: 3, + stato: 'buono', + posizione: 'scaffale A', + note: null, + }, + ]); + }); + + test('org diverse ottengono query filtrate su org_id diversi', async () => { + magazzinoVoceFindMany.mockResolvedValue([]); + + await request(app).get('/magazzino').set('Authorization', `Bearer ${tokenOrg('org-a')}`); + await request(app).get('/magazzino').set('Authorization', `Bearer ${tokenOrg('org-b')}`); + + expect(magazzinoVoceFindMany).toHaveBeenNthCalledWith(1, expect.objectContaining({ where: { orgId: 'org-a' } })); + expect(magazzinoVoceFindMany).toHaveBeenNthCalledWith(2, expect.objectContaining({ where: { orgId: 'org-b' } })); + }); + + test('risponde 401 senza token', async () => { + const response = await request(app).get('/magazzino'); + + expect(response.status).toBe(401); + expect(magazzinoVoceFindMany).not.toHaveBeenCalled(); + }); +}); + +describe('POST /magazzino', () => { + test('aggiunge una voce per l\'org corrente se il materiale è approvato', async () => { + materialeFindUnique.mockResolvedValueOnce(materialeApprovato()); + magazzinoVoceCreate.mockResolvedValueOnce(voceConMateriale()); + + const response = await request(app) + .post('/magazzino') + .set('Authorization', `Bearer ${tokenOrg('org-a')}`) + .send({ materialeId: 'm-1', quantitaPosseduta: 3, stato: 'buono', posizione: 'scaffale A' }); + + expect(response.status).toBe(201); + expect(magazzinoVoceCreate).toHaveBeenCalledWith({ + data: { + materialeId: 'm-1', + quantitaPosseduta: 3, + stato: 'buono', + posizione: 'scaffale A', + note: undefined, + orgId: 'org-a', + }, + include: { materiale: true }, + }); + }); + + test('ignora un eventuale org_id inviato dal client, usa sempre quello del token', async () => { + materialeFindUnique.mockResolvedValueOnce(materialeApprovato()); + magazzinoVoceCreate.mockResolvedValueOnce(voceConMateriale()); + + await request(app) + .post('/magazzino') + .set('Authorization', `Bearer ${tokenOrg('org-a')}`) + .send({ materialeId: 'm-1', quantitaPosseduta: 3, stato: 'buono', orgId: 'org-spoofed' }); + + expect(magazzinoVoceCreate).toHaveBeenCalledWith( + expect.objectContaining({ data: expect.objectContaining({ orgId: 'org-a' }) }), + ); + }); + + test('risponde 400 e invita a proporre il materiale se non esiste nel catalogo', async () => { + materialeFindUnique.mockResolvedValueOnce(null); + + const response = await request(app) + .post('/magazzino') + .set('Authorization', `Bearer ${tokenOrg('org-a')}`) + .send({ materialeId: 'inesistente', quantitaPosseduta: 1, stato: 'buono' }); + + expect(response.status).toBe(400); + expect(response.body.message).toMatch(/POST \/materiali\/proposte/); + expect(magazzinoVoceCreate).not.toHaveBeenCalled(); + }); + + test('risponde 400 se il materiale esiste ma non è ancora approvato', async () => { + materialeFindUnique.mockResolvedValueOnce({ ...materialeApprovato(), stato: 'proposto' }); + + const response = await request(app) + .post('/magazzino') + .set('Authorization', `Bearer ${tokenOrg('org-a')}`) + .send({ materialeId: 'm-1', quantitaPosseduta: 1, stato: 'buono' }); + + expect(response.status).toBe(400); + expect(response.body.message).toMatch(/POST \/materiali\/proposte/); + expect(magazzinoVoceCreate).not.toHaveBeenCalled(); + }); + + test('risponde 400 se lo stato non è uno dei valori validi', async () => { + const response = await request(app) + .post('/magazzino') + .set('Authorization', `Bearer ${tokenOrg('org-a')}`) + .send({ materialeId: 'm-1', quantitaPosseduta: 1, stato: 'ottimo' }); + + expect(response.status).toBe(400); + expect(materialeFindUnique).not.toHaveBeenCalled(); + expect(magazzinoVoceCreate).not.toHaveBeenCalled(); + }); + + test('risponde 401 senza token', async () => { + const response = await request(app).post('/magazzino').send({ materialeId: 'm-1', quantitaPosseduta: 1, stato: 'buono' }); + + expect(response.status).toBe(401); + expect(magazzinoVoceCreate).not.toHaveBeenCalled(); + }); +}); + +describe('PUT /magazzino/:id — isolamento tra org', () => { + test('un\'org non può modificare una voce di un\'altra org (404, non 403)', async () => { + magazzinoVoceFindFirst.mockResolvedValueOnce(null); + + const response = await request(app) + .put('/magazzino/mv-1') + .set('Authorization', `Bearer ${tokenOrg('org-b')}`) + .send({ quantitaPosseduta: 5 }); + + expect(response.status).toBe(404); + expect(magazzinoVoceFindFirst).toHaveBeenCalledWith( + expect.objectContaining({ where: { id: 'mv-1', orgId: 'org-b' } }), + ); + expect(magazzinoVoceUpdate).not.toHaveBeenCalled(); + }); + + test('l\'org proprietaria può modificare la propria voce', async () => { + magazzinoVoceFindFirst.mockResolvedValueOnce(voceConMateriale()); + magazzinoVoceUpdate.mockResolvedValueOnce(voceConMateriale({ quantitaPosseduta: 5 })); + + const response = await request(app) + .put('/magazzino/mv-1') + .set('Authorization', `Bearer ${tokenOrg('org-a')}`) + .send({ quantitaPosseduta: 5 }); + + expect(response.status).toBe(200); + expect(response.body.quantitaPosseduta).toBe(5); + expect(magazzinoVoceUpdate).toHaveBeenCalledWith({ + where: { id: 'mv-1' }, + data: { quantitaPosseduta: 5 }, + include: { materiale: true }, + }); + }); + + test('se si cambia materialeId, valida di nuovo che sia approvato', async () => { + magazzinoVoceFindFirst.mockResolvedValueOnce(voceConMateriale()); + materialeFindUnique.mockResolvedValueOnce({ ...materialeApprovato('m-2'), stato: 'proposto' }); + + const response = await request(app) + .put('/magazzino/mv-1') + .set('Authorization', `Bearer ${tokenOrg('org-a')}`) + .send({ materialeId: 'm-2' }); + + expect(response.status).toBe(400); + expect(response.body.message).toMatch(/POST \/materiali\/proposte/); + expect(magazzinoVoceUpdate).not.toHaveBeenCalled(); + }); + + test('risponde 401 senza token', async () => { + const response = await request(app).put('/magazzino/mv-1').send({ quantitaPosseduta: 1 }); + + expect(response.status).toBe(401); + expect(magazzinoVoceFindFirst).not.toHaveBeenCalled(); + }); +}); + +describe('DELETE /magazzino/:id — isolamento tra org', () => { + test('un\'org non può eliminare una voce di un\'altra org', async () => { + magazzinoVoceFindFirst.mockResolvedValueOnce(null); + + const response = await request(app).delete('/magazzino/mv-1').set('Authorization', `Bearer ${tokenOrg('org-b')}`); + + expect(response.status).toBe(404); + expect(magazzinoVoceDelete).not.toHaveBeenCalled(); + }); + + test('l\'org proprietaria può eliminare la propria voce', async () => { + magazzinoVoceFindFirst.mockResolvedValueOnce(voceConMateriale()); + magazzinoVoceDelete.mockResolvedValueOnce({ id: 'mv-1' }); + + const response = await request(app).delete('/magazzino/mv-1').set('Authorization', `Bearer ${tokenOrg('org-a')}`); + + expect(response.status).toBe(204); + expect(magazzinoVoceDelete).toHaveBeenCalledWith({ where: { id: 'mv-1' } }); + }); + + test('risponde 401 senza token', async () => { + const response = await request(app).delete('/magazzino/mv-1'); + + expect(response.status).toBe(401); + expect(magazzinoVoceDelete).not.toHaveBeenCalled(); + }); +}); diff --git a/scouthub-magazzino-be/tests/integration/materiali.endpoint.test.ts b/scouthub-magazzino-be/tests/integration/materiali.endpoint.test.ts new file mode 100644 index 0000000..9c93fc8 --- /dev/null +++ b/scouthub-magazzino-be/tests/integration/materiali.endpoint.test.ts @@ -0,0 +1,312 @@ +import { generateKeyPairSync } from 'crypto'; +import request from 'supertest'; +import nock from 'nock'; +import jwt from 'jsonwebtoken'; + +process.env.KEYCLOAK_BASE_URL = 'http://keycloak.test'; +process.env.KEYCLOAK_REALM = 'scouthub'; +process.env.KEYCLOAK_MAGAZZINO_CLIENT_ID = 'test-client'; +process.env.KEYCLOAK_MAGAZZINO_CLIENT_SECRET = 'test-secret'; +process.env.DATABASE_URL = 'postgresql://user:pass@localhost:5432/scouthub_magazzino_test'; + +const materialeFindMany = jest.fn(); +const materialeFindUnique = jest.fn(); +const materialeCreate = jest.fn(); +const materialeUpdate = jest.fn(); + +jest.mock('../../src/db/prisma', () => ({ + prisma: { + materiale: { + findMany: (...args: unknown[]) => materialeFindMany(...args), + findUnique: (...args: unknown[]) => materialeFindUnique(...args), + create: (...args: unknown[]) => materialeCreate(...args), + update: (...args: unknown[]) => materialeUpdate(...args), + }, + }, +})); + +import { app } from '../../src/app'; + +const KEYCLOAK_HOST = 'http://keycloak.test'; +const CERTS_PATH = '/realms/scouthub/protocol/openid-connect/certs'; +const KID = 'test-kid'; + +const { publicKey, privateKey } = generateKeyPairSync('rsa', { modulusLength: 2048 }); +const jwk = publicKey.export({ format: 'jwk' }) as Record; +const privateKeyPem = privateKey.export({ type: 'pkcs1', format: 'pem' }) as string; + +function signToken(payload: object): string { + return jwt.sign(payload, privateKeyPem, { algorithm: 'RS256', keyid: KID, expiresIn: '5m' }); +} + +function utenteToken(orgId = 'org-1'): string { + return signToken({ + sub: 'user-1', + realm_access: { roles: ['censito'] }, + organization: { 'gruppo-alfa': { id: orgId, roles: [] } }, + }); +} + +function adminCatalogoToken(orgId = 'org-9'): string { + return signToken({ + sub: 'admin-1', + realm_access: { roles: ['moderatore'] }, + organization: { 'gruppo-omega': { id: orgId, roles: [] } }, + }); +} + +beforeAll(() => { + nock(KEYCLOAK_HOST).persist().get(CERTS_PATH).reply(200, { + keys: [{ ...jwk, kid: KID, alg: 'RS256', use: 'sig' }], + }); +}); + +afterAll(() => { + nock.cleanAll(); +}); + +beforeEach(() => { + jest.clearAllMocks(); +}); + +describe('GET /materiali', () => { + test('non richiede autenticazione e restituisce solo i materiali approvati', async () => { + materialeFindMany.mockResolvedValueOnce([ + { + id: 'm-1', + nome: 'Tenda canadese', + categoria: 'campeggio', + unitaMisura: 'pz', + stato: 'approvato', + propostoDaOrgId: 'org-x', + creatoIl: new Date('2026-01-01'), + }, + ]); + + const response = await request(app).get('/materiali'); + + expect(response.status).toBe(200); + expect(response.body).toEqual([{ id: 'm-1', nome: 'Tenda canadese', categoria: 'campeggio', unitaMisura: 'pz' }]); + expect(materialeFindMany).toHaveBeenCalledWith({ + where: { stato: 'approvato' }, + orderBy: { nome: 'asc' }, + }); + }); + + test('filtra per categoria quando richiesto', async () => { + materialeFindMany.mockResolvedValueOnce([]); + + await request(app).get('/materiali').query({ categoria: 'cucina' }); + + expect(materialeFindMany).toHaveBeenCalledWith({ + where: { stato: 'approvato', categoria: 'cucina' }, + orderBy: { nome: 'asc' }, + }); + }); + + test('il catalogo pubblico esclude sempre le proposte non approvate, anche forzando uno stato via query string', async () => { + materialeFindMany.mockResolvedValueOnce([]); + + await request(app).get('/materiali').query({ stato: 'proposto' }); + + // Il filtro "stato" non è un parametro accettato: la query verso il database + // continua a chiedere solo lo stato "approvato". + expect(materialeFindMany).toHaveBeenCalledWith({ + where: { stato: 'approvato' }, + orderBy: { nome: 'asc' }, + }); + }); +}); + +describe('POST /materiali/proposte', () => { + test('salva la proposta con stato "proposto" e proposto_da_org_id preso dal token', async () => { + materialeCreate.mockResolvedValueOnce({ + id: 'm-2', + nome: 'Fornello a gas', + categoria: 'cucina', + unitaMisura: 'pz', + stato: 'proposto', + propostoDaOrgId: 'org-1', + creatoIl: new Date('2026-01-02'), + }); + + const response = await request(app) + .post('/materiali/proposte') + .set('Authorization', `Bearer ${utenteToken('org-1')}`) + .send({ nome: 'Fornello a gas', categoria: 'cucina', unitaMisura: 'pz' }); + + expect(response.status).toBe(201); + expect(response.body).toMatchObject({ propostoDaOrgId: 'org-1', stato: 'proposto' }); + expect(materialeCreate).toHaveBeenCalledWith({ + data: { nome: 'Fornello a gas', categoria: 'cucina', unitaMisura: 'pz', propostoDaOrgId: 'org-1', stato: 'proposto' }, + }); + }); + + test('ignora un eventuale proposto_da_org_id inviato dal client, usa sempre quello del token', async () => { + materialeCreate.mockResolvedValueOnce({ + id: 'm-3', + nome: 'Piccone', + categoria: 'attrezzi', + unitaMisura: 'pz', + stato: 'proposto', + propostoDaOrgId: 'org-1', + creatoIl: new Date('2026-01-03'), + }); + + await request(app) + .post('/materiali/proposte') + .set('Authorization', `Bearer ${utenteToken('org-1')}`) + .send({ nome: 'Piccone', categoria: 'attrezzi', unitaMisura: 'pz', propostoDaOrgId: 'org-spoofed' }); + + expect(materialeCreate).toHaveBeenCalledWith({ + data: expect.objectContaining({ propostoDaOrgId: 'org-1' }), + }); + }); + + test('risponde 401 senza token', async () => { + const response = await request(app) + .post('/materiali/proposte') + .send({ nome: 'Fornello', categoria: 'cucina', unitaMisura: 'pz' }); + + expect(response.status).toBe(401); + expect(materialeCreate).not.toHaveBeenCalled(); + }); + + test("risponde 400 se manca un campo obbligatorio", async () => { + const response = await request(app) + .post('/materiali/proposte') + .set('Authorization', `Bearer ${utenteToken()}`) + .send({ nome: 'Fornello' }); + + expect(response.status).toBe(400); + expect(materialeCreate).not.toHaveBeenCalled(); + }); +}); + +describe('GET /materiali/proposte', () => { + test('un utente normale non può accedere', async () => { + const response = await request(app).get('/materiali/proposte').set('Authorization', `Bearer ${utenteToken()}`); + + expect(response.status).toBe(403); + expect(materialeFindMany).not.toHaveBeenCalled(); + }); + + test('un moderatore vede tutte le proposte pending', async () => { + materialeFindMany.mockResolvedValueOnce([ + { + id: 'm-4', + nome: 'Corda', + categoria: 'attrezzi', + unitaMisura: 'm', + stato: 'proposto', + propostoDaOrgId: 'org-7', + creatoIl: new Date('2026-01-04'), + }, + ]); + + const response = await request(app).get('/materiali/proposte').set('Authorization', `Bearer ${adminCatalogoToken()}`); + + expect(response.status).toBe(200); + expect(materialeFindMany).toHaveBeenCalledWith({ + where: { stato: 'proposto' }, + orderBy: { creatoIl: 'asc' }, + }); + expect(response.body).toEqual([ + expect.objectContaining({ id: 'm-4', propostoDaOrgId: 'org-7', stato: 'proposto' }), + ]); + }); + + test('risponde 401 senza token', async () => { + const response = await request(app).get('/materiali/proposte'); + + expect(response.status).toBe(401); + expect(materialeFindMany).not.toHaveBeenCalled(); + }); +}); + +describe('PATCH /materiali/proposte/:id', () => { + test('un utente normale non può decidere una proposta', async () => { + const response = await request(app) + .patch('/materiali/proposte/m-1') + .set('Authorization', `Bearer ${utenteToken()}`) + .send({ decisione: 'approvato' }); + + expect(response.status).toBe(403); + expect(materialeUpdate).not.toHaveBeenCalled(); + }); + + test('un moderatore può approvare una proposta pending', async () => { + materialeFindUnique.mockResolvedValueOnce({ id: 'm-1', stato: 'proposto' }); + materialeUpdate.mockResolvedValueOnce({ + id: 'm-1', + nome: 'Tenda', + categoria: 'campeggio', + unitaMisura: 'pz', + stato: 'approvato', + propostoDaOrgId: 'org-1', + creatoIl: new Date('2026-01-01'), + }); + + const response = await request(app) + .patch('/materiali/proposte/m-1') + .set('Authorization', `Bearer ${adminCatalogoToken()}`) + .send({ decisione: 'approvato' }); + + expect(response.status).toBe(200); + expect(response.body.stato).toBe('approvato'); + expect(materialeUpdate).toHaveBeenCalledWith({ where: { id: 'm-1' }, data: { stato: 'approvato' } }); + }); + + test('un moderatore può rifiutare una proposta pending', async () => { + materialeFindUnique.mockResolvedValueOnce({ id: 'm-2', stato: 'proposto' }); + materialeUpdate.mockResolvedValueOnce({ + id: 'm-2', + nome: 'Zaino', + categoria: 'equipaggiamento', + unitaMisura: 'pz', + stato: 'rifiutato', + propostoDaOrgId: 'org-2', + creatoIl: new Date('2026-01-01'), + }); + + const response = await request(app) + .patch('/materiali/proposte/m-2') + .set('Authorization', `Bearer ${adminCatalogoToken()}`) + .send({ decisione: 'rifiutato' }); + + expect(response.status).toBe(200); + expect(response.body.stato).toBe('rifiutato'); + expect(materialeUpdate).toHaveBeenCalledWith({ where: { id: 'm-2' }, data: { stato: 'rifiutato' } }); + }); + + test('risponde 404 se la proposta non esiste', async () => { + materialeFindUnique.mockResolvedValueOnce(null); + + const response = await request(app) + .patch('/materiali/proposte/inesistente') + .set('Authorization', `Bearer ${adminCatalogoToken()}`) + .send({ decisione: 'approvato' }); + + expect(response.status).toBe(404); + expect(materialeUpdate).not.toHaveBeenCalled(); + }); + + test('risponde 409 se la proposta è già stata decisa', async () => { + materialeFindUnique.mockResolvedValueOnce({ id: 'm-1', stato: 'approvato' }); + + const response = await request(app) + .patch('/materiali/proposte/m-1') + .set('Authorization', `Bearer ${adminCatalogoToken()}`) + .send({ decisione: 'rifiutato' }); + + expect(response.status).toBe(409); + expect(materialeUpdate).not.toHaveBeenCalled(); + }); + + test('risponde 401 senza token', async () => { + const response = await request(app).patch('/materiali/proposte/m-1').send({ decisione: 'approvato' }); + + expect(response.status).toBe(401); + expect(materialeUpdate).not.toHaveBeenCalled(); + }); +}); diff --git a/scouthub-magazzino-be/tests/integration/tipiEvento.endpoint.test.ts b/scouthub-magazzino-be/tests/integration/tipiEvento.endpoint.test.ts new file mode 100644 index 0000000..cb3c671 --- /dev/null +++ b/scouthub-magazzino-be/tests/integration/tipiEvento.endpoint.test.ts @@ -0,0 +1,161 @@ +import { generateKeyPairSync } from 'crypto'; +import request from 'supertest'; +import nock from 'nock'; +import jwt from 'jsonwebtoken'; + +process.env.KEYCLOAK_BASE_URL = 'http://keycloak.test'; +process.env.KEYCLOAK_REALM = 'scouthub'; +process.env.KEYCLOAK_MAGAZZINO_CLIENT_ID = 'test-client'; +process.env.KEYCLOAK_MAGAZZINO_CLIENT_SECRET = 'test-secret'; +process.env.DATABASE_URL = 'postgresql://user:pass@localhost:5432/scouthub_magazzino_test'; + +const tipoEventoFindMany = jest.fn(); +const tipoEventoCreate = jest.fn(); +const tipoEventoUpdate = jest.fn(); +const tipoEventoDelete = jest.fn(); + +jest.mock('../../src/db/prisma', () => ({ + prisma: { + tipoEvento: { + findMany: (...args: unknown[]) => tipoEventoFindMany(...args), + create: (...args: unknown[]) => tipoEventoCreate(...args), + update: (...args: unknown[]) => tipoEventoUpdate(...args), + delete: (...args: unknown[]) => tipoEventoDelete(...args), + }, + }, +})); + +import { app } from '../../src/app'; + +const KEYCLOAK_HOST = 'http://keycloak.test'; +const CERTS_PATH = '/realms/scouthub/protocol/openid-connect/certs'; +const KID = 'test-kid'; + +const { publicKey, privateKey } = generateKeyPairSync('rsa', { modulusLength: 2048 }); +const jwk = publicKey.export({ format: 'jwk' }) as Record; +const privateKeyPem = privateKey.export({ type: 'pkcs1', format: 'pem' }) as string; + +function signToken(payload: object): string { + return jwt.sign(payload, privateKeyPem, { algorithm: 'RS256', keyid: KID, expiresIn: '5m' }); +} + +function utenteToken(): string { + return signToken({ + sub: 'user-1', + realm_access: { roles: ['censito'] }, + organization: { 'gruppo-alfa': { id: 'org-1', roles: [] } }, + }); +} + +function adminCatalogoToken(): string { + return signToken({ + sub: 'admin-1', + realm_access: { roles: ['moderatore'] }, + organization: { 'gruppo-omega': { id: 'org-9', roles: [] } }, + }); +} + +beforeAll(() => { + nock(KEYCLOAK_HOST).persist().get(CERTS_PATH).reply(200, { + keys: [{ ...jwk, kid: KID, alg: 'RS256', use: 'sig' }], + }); +}); + +afterAll(() => { + nock.cleanAll(); +}); + +beforeEach(() => { + jest.clearAllMocks(); +}); + +describe('GET /tipi-evento', () => { + test('è pubblico e restituisce la lista', async () => { + tipoEventoFindMany.mockResolvedValueOnce([{ id: 't-1', nome: 'Campo estivo' }]); + + const response = await request(app).get('/tipi-evento'); + + expect(response.status).toBe(200); + expect(response.body).toEqual([{ id: 't-1', nome: 'Campo estivo' }]); + }); +}); + +describe('POST /tipi-evento', () => { + test('un utente normale non può creare un tipo evento', async () => { + const response = await request(app) + .post('/tipi-evento') + .set('Authorization', `Bearer ${utenteToken()}`) + .send({ nome: 'Bivacco' }); + + expect(response.status).toBe(403); + expect(tipoEventoCreate).not.toHaveBeenCalled(); + }); + + test('un moderatore può creare un tipo evento', async () => { + tipoEventoCreate.mockResolvedValueOnce({ id: 't-2', nome: 'Bivacco' }); + + const response = await request(app) + .post('/tipi-evento') + .set('Authorization', `Bearer ${adminCatalogoToken()}`) + .send({ nome: 'Bivacco' }); + + expect(response.status).toBe(201); + expect(tipoEventoCreate).toHaveBeenCalledWith({ data: { nome: 'Bivacco' } }); + }); + + test('risponde 401 senza token', async () => { + const response = await request(app).post('/tipi-evento').send({ nome: 'Bivacco' }); + + expect(response.status).toBe(401); + expect(tipoEventoCreate).not.toHaveBeenCalled(); + }); +}); + +describe('PUT /tipi-evento/:id', () => { + test('un utente normale non può modificare un tipo evento', async () => { + const response = await request(app) + .put('/tipi-evento/t-1') + .set('Authorization', `Bearer ${utenteToken()}`) + .send({ nome: 'Uscita di branco' }); + + expect(response.status).toBe(403); + expect(tipoEventoUpdate).not.toHaveBeenCalled(); + }); + + test('un moderatore può modificare un tipo evento', async () => { + tipoEventoUpdate.mockResolvedValueOnce({ id: 't-1', nome: 'Uscita di branco' }); + + const response = await request(app) + .put('/tipi-evento/t-1') + .set('Authorization', `Bearer ${adminCatalogoToken()}`) + .send({ nome: 'Uscita di branco' }); + + expect(response.status).toBe(200); + expect(tipoEventoUpdate).toHaveBeenCalledWith({ where: { id: 't-1' }, data: { nome: 'Uscita di branco' } }); + }); +}); + +describe('DELETE /tipi-evento/:id', () => { + test('un utente normale non può eliminare un tipo evento', async () => { + const response = await request(app).delete('/tipi-evento/t-1').set('Authorization', `Bearer ${utenteToken()}`); + + expect(response.status).toBe(403); + expect(tipoEventoDelete).not.toHaveBeenCalled(); + }); + + test('un moderatore può eliminare un tipo evento', async () => { + tipoEventoDelete.mockResolvedValueOnce({ id: 't-1', nome: 'Sede' }); + + const response = await request(app).delete('/tipi-evento/t-1').set('Authorization', `Bearer ${adminCatalogoToken()}`); + + expect(response.status).toBe(204); + expect(tipoEventoDelete).toHaveBeenCalledWith({ where: { id: 't-1' } }); + }); + + test('risponde 401 senza token', async () => { + const response = await request(app).delete('/tipi-evento/t-1'); + + expect(response.status).toBe(401); + expect(tipoEventoDelete).not.toHaveBeenCalled(); + }); +}); diff --git a/scouthub-magazzino-be/tsconfig.jest.json b/scouthub-magazzino-be/tsconfig.jest.json new file mode 100644 index 0000000..45223ea --- /dev/null +++ b/scouthub-magazzino-be/tsconfig.jest.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "rootDir": ".", + "noEmit": true, + "declaration": false + }, + "include": ["src/**/*.ts", "tests/**/*.ts"] +} diff --git a/scouthub-magazzino-be/tsconfig.json b/scouthub-magazzino-be/tsconfig.json new file mode 100644 index 0000000..24da6d8 --- /dev/null +++ b/scouthub-magazzino-be/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "CommonJS", + "lib": ["ES2022"], + "outDir": "dist", + "rootDir": "src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "moduleResolution": "node", + "declaration": false, + "sourceMap": true + }, + "include": ["src/**/*.ts"], + "exclude": ["node_modules", "dist"] +}