diff --git a/.env.example b/.env.example index c2b7a5c..8b10dbd 100644 --- a/.env.example +++ b/.env.example @@ -1,10 +1,92 @@ -APP_ENV=development #development / test / prod -PORT=3069 -DATABASE_URL=postgresql://USERNAME:PASSWORD@HOST:5432/DATABASE?schema=public +# RayLab Core - contoh konfigurasi environment +# Salin file ini menjadi .env dan isi nilai-nilai sensitif sesuai environment Anda. +# Jangan commit .env yang berisi secrets ke VCS. -JWT_SECRET=GANTI_DENGAN_SECRET -JWT_REFRESH_SECRET=GANTI_DENGAN_REFRESH_SECRET +######################## +# Database (Postgres) +######################## +# URL koneksi Postgres untuk Prisma. +# Contoh: postgresql://user:password@localhost:5432/raylab +DATABASE_URL="postgresql://raylab:password@localhost:5432/raylab" -SWAGGER_ENABLED=true +# Catatan penting: migration menggunakan fungsi UUID di migration SQL. +# Pastikan database memiliki ekstensi yang sesuai: +# - Jika migration menggunakan gen_random_uuid(): pasang pgcrypto +# SQL: CREATE EXTENSION IF NOT EXISTS pgcrypto; +# - Jika ingin gunakan uuid_generate_v4(): pasang uuid-ossp +# SQL: CREATE EXTENSION IF NOT EXISTS \"uuid-ossp\"; -LOG_LEVEL=debug \ No newline at end of file +######################## +# Internal JWT (RayLab) +######################## +# Secret untuk menandatangani JWT internal (hingga JwtModule digunakan). +# Ganti dengan secret yang kuat di produksi. +RAYLAB_JWT_SECRET="replace-with-a-strong-secret" + +######################## +# Authentik / OIDC (Identity Provider) +######################## +# Issuer base URL dari Authentik / OIDC provider (wajib untuk verifikasi token) +# Contoh: https://auth.example.com +AUTHENTIK_ISSUER="https://auth.example.com" + +# Audience (aud) yang diharapkan pada token OIDC (opsional) +AUTHENTIK_AUDIENCE="raylab" + +# JWKS URI jika ingin override. Jika kosong, akan dibentuk dari AUTHENTIK_ISSUER + "/.well-known/jwks.json" +AUTHENTIK_JWKS_URI="" + +######################## +# Redis (opsional, beberapa fitur) +######################## +# Aktifkan/Nonaktifkan Redis (default true). +REDIS_ENABLED="true" + +# URL Redis (contoh: redis://localhost:6379). Hanya dipakai bila REDIS_ENABLED=true +REDIS_URL="redis://localhost:6379" + +######################## +# Server / Aplikasi +######################## +# Base URL aplikasi (dipakai pada integration tests / helper) +BASE_URL="http://localhost:3000" + +# Port aplikasi +PORT="3000" + +# Node environment +NODE_ENV="development" + +# Log level (debug/info/warn/error) +LOG_LEVEL="debug" + +######################## +# Admin / Test accounts (opsional, dipakai tests/integration jika diperlukan) +######################## +# Akun admin untuk keperluan pengujian/integrasi (hanya contoh) +ADMIN_USERNAME="admin" +ADMIN_PASSWORD="changeme" + +# Informasi akun uji (opsional) +TEST_USER_EMAIL="test-integration@example.com" +TEST_USER_USERNAME="test-integration" +TEST_USER_PASSWORD="StrongP@ssw0rd!" + +# Expire time for test tokens (detik) +TEST_TOKEN_EXPIRES_IN="3600" + +######################## +# Optional / Integration hints +######################## +# Jika Anda menggunakan penyedia lain atau menambahkan variabel tambahan, +# tambahkan di sini. Contoh: +# SSO_CALLBACK_URL="http://localhost:3000/auth/callback" +# MAILER_* variables, STORAGE_*, dsb. + +######################## +# Security reminders +######################## +# - Jangan commit file .env dengan secrets. +# - Gunakan secret manager di production (Vault, AWS Secrets Manager, ..). +# - Pastikan DATABASE_URL menunjuk ke database yang benar dan aman. +# - Pastikan AUTHENTIK_ISSUER dan JWKS dapat diakses dari RayLab instance. \ No newline at end of file diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml new file mode 100644 index 0000000..21bee14 --- /dev/null +++ b/.gitea/workflows/deploy.yml @@ -0,0 +1,24 @@ +name: Deploy + +on: + push: + branches: + - main + +jobs: + deploy: + runs-on: ubuntu-latest + + steps: + - name: Deploy + run: | + git config --global --add safe.directory /DATA/git/RayLab-Core + + cd /DATA/git/RayLab-Core + + git fetch origin + git reset --hard origin/main + + cd /DATA/docker/raylab-core + + docker compose up -d --build diff --git a/.gitignore b/.gitignore index d91abad..241a13f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ node_modules RayLab-Core.zip RayLab-Core.rar -.env \ No newline at end of file +.env +dist/ diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..5cab610 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,20 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Debug NestJS", + "type": "node", + "request": "launch", + "runtimeExecutable": "npm", + "runtimeArgs": [ + "run", + "start:debug" + ], + "console": "integratedTerminal", + "restart": true, + "skipFiles": [ + "/**" + ] + } + ] +} \ No newline at end of file diff --git a/Target V1.txt b/Target V1.txt new file mode 100644 index 0000000..602ac97 --- /dev/null +++ b/Target V1.txt @@ -0,0 +1,25 @@ +RayLab Core +│ +├── Identity +│ ├── User +│ ├── Group +│ ├── Role +│ └── Permission +│ +├── Authentication +│ +├── Authorization +│ +├── Audit +│ +├── Scheduler +│ +├── Media +│ +├── Storage +│ +├── Configuration +│ +├── Workflow / Events +│ +└── Registry \ No newline at end of file diff --git a/api_spec.txt b/api_spec.txt new file mode 100644 index 0000000..20b2fb8 --- /dev/null +++ b/api_spec.txt @@ -0,0 +1,208 @@ +API Spec - Identity & Authorization API (Bahasa Indonesia) + +Ringkasan +-------- +Spesifikasi berikut disesuaikan dengan controller aktual pada aplikasi: modul Auth (OIDC + internal JWT), modul Identity (Users, Roles, Permissions) dan mekanisme otorisasi berbasis permission. + +Base URL +-------- +- https://api.example.com + +Header Umum & Auth +------------------ +- Content-Type: application/json +- Accept: application/json +- Otentikasi dapat diberikan dalam dua cara: + 1) Cookie internal (default flow): + - raylab_jwt (internal JWT, httpOnly cookie) + - raylab_refresh (internal refresh token, httpOnly cookie) + 2) Authorization header: Bearer +- Banyak endpoint melindungi akses dengan JWT dan permission checks. Respons sukses dari controller umumnya dibungkus sebagai: + { "success": true, "data": ..., "meta": { ... } } + +Auth (modul /auth) +------------------- +1) GET /auth/login +- Deskripsi: Mulai flow Authorization Code + PKCE. Redirect (302) ke Identity Provider. +- Query params: + - returnTo (optional) - URL tujuan setelah login +- Response: 302 Redirect + +2) GET /auth/callback +- Deskripsi: Endpoint callback OIDC. Menukarkan code/state, membuat internal JWT & refresh token, dan menyetel cookie httpOnly. +- Response: 302 Redirect ke returnTo atau '/' +- Cookie yang disetel: + - raylab_jwt (internal JWT, maxAge sesuai expiresIn) + - raylab_refresh (refresh token) + +3) POST /auth/logout +- Deskripsi: Invalidate internal refresh token (opsional) dan redirect ke logout identity provider. +- Body (optional): { "refreshToken": "..." } +- Response: 302 Redirect + +4) POST /auth/refresh +- Deskripsi: Tukar refresh token menjadi internal JWT baru. +- Input: refresh token di cookie raylab_refresh atau di body { "refreshToken": "..." } +- Response 200: { "success": true, "data": { "accessToken": "...", "expiresIn": 3600, ... } } + +5) GET /auth/me +- Deskripsi: Ambil data user saat ini dari internal JWT (cookie atau Authorization header). +- Response 200: { "success": true, "data": { /* user object */ } } + +Catatan: tidak ada endpoint "/auth/register" atau POST /auth/login berbasis email/password pada controller saat ini — login terjadi via OIDC dan internal session cookies. + +Users (modul /users) +-------------------- +Semua endpoint Users dijalankan di bawah guards: JwtAuthGuard, CurrentUserGuard dan PermissionGuard. Respons mengikuti format { success, data, meta }. + +1) GET /users +- Permission: PermissionType.USER_READ +- Deskripsi: List pengguna (paged) +- Query params umum: page, per_page, sort, q +- Response 200: + { "success": true, "data": [ /* user list (toResponse) */ ], "meta": { "total": 123 } } + +2) GET /users/me +- Deskripsi: Ambil profil user saat ini (token harus ada di cookie atau header) +- Response 200: { "success": true, "data": { /* current user */ }, "meta": {} } + +3) GET /users/:id +- Permission: PermissionType.USER_READ +- Deskripsi: Ambil user berdasarkan id +- Response 200: { "success": true, "data": { /* user */ }, "meta": {} } +- 404 jika tidak ditemukan + +4) PATCH /users/:id/enable +- Permission: PermissionType.USER_UPDATE +- Deskripsi: Enable user +- Response 200: { "success": true, "data": { /* updated user */ }, "meta": {} } + +5) PATCH /users/:id/disable +- Permission: PermissionType.USER_UPDATE +- Deskripsi: Disable user +- Response 200 + +6) PATCH /users/:id +- Permission: PermissionType.USER_UPDATE +- Deskripsi: Update profil user (body berisi fields yang diperbolehkan) +- Body contoh: { "name": "Nama Baru", "email": "baru@example.com" } +- Response 200: { "success": true, "data": { /* updated user */ }, "meta": {} } + +7) DELETE /users/:id +- Permission: PermissionType.USER_DELETE +- Deskripsi: Soft delete user +- Response 200: { "success": true, "data": null, "meta": {} } + +8) POST /users/:id/restore +- Permission: PermissionType.USER_UPDATE +- Deskripsi: Restore user yang di-soft-delete +- Response 200: { "success": true, "data": { /* restored user */ }, "meta": {} } + +Roles (modul /roles) +-------------------- +Semua route dilindungi oleh JwtAuthGuard, CurrentUserGuard dan PermissionGuard. + +1) GET /roles +- Permission: PermissionType.ROLE_READ +- Deskripsi: List roles +- Query params: page, per_page, q +- Response 200: { "success": true, "data": [ /* roles */ ], "meta": { "total": 10 } } + +2) GET /roles/:id +- Permission: PermissionType.ROLE_READ +- Deskripsi: Ambil role +- Response 200 + +3) POST /roles +- Permission: PermissionType.ROLE_CREATE +- Deskripsi: Buat role baru +- Body contoh: { "name": "editor", "description": "..." } +- Response 200: { "success": true, "data": { /* created role */ }, "meta": {} } + +4) PATCH /roles/:id +- Permission: PermissionType.ROLE_UPDATE +- Deskripsi: Update role +- Response 200 + +5) DELETE /roles/:id +- Permission: PermissionType.ROLE_DELETE +- Deskripsi: Hapus role +- Response 200: { "success": true, "data": null, "meta": {} } + +6) POST /roles/:id/permissions +- Permission: PermissionType.ROLE_UPDATE +- Deskripsi: Assign permission ke role +- Body: { "permissionId": "..." } +- Response 200: { "success": true, "data": { /* updated role */ }, "meta": {} } + +7) DELETE /roles/:id/permissions/:permissionId +- Permission: PermissionType.ROLE_UPDATE +- Deskripsi: Remove permission dari role +- Response 200 + +Permissions (modul /permissions) +------------------------------- +Semua route dilindungi oleh JwtAuthGuard, CurrentUserGuard dan PermissionGuard. + +1) GET /permissions +- Permission: PermissionType.PERMISSION_READ +- Deskripsi: List permissions +- Response 200: { "success": true, "data": [ /* permissions */ ], "meta": { "total": 20 } } + +2) GET /permissions/:id +- Permission: PermissionType.PERMISSION_READ +- Deskripsi: Ambil permission +- Response 200 + +3) POST /permissions +- Permission: PermissionType.PERMISSION_CREATE +- Deskripsi: Buat permission baru +- Body contoh: { "name": "user:create", "description": "..." } +- Response 200: { "success": true, "data": { /* created */ }, "meta": {} } + +4) PATCH /permissions/:id +- Permission: PermissionType.PERMISSION_UPDATE +- Deskripsi: Update permission +- Response 200 + +5) DELETE /permissions/:id +- Permission: PermissionType.PERMISSION_DELETE +- Deskripsi: Delete permission +- Response 200: { "success": true, "data": null, "meta": {} } + +Format Tanggal dan Numerik +-------------------------- +- Tanggal/waktu: ISO 8601 (UTC), contoh: "2024-08-01T12:34:56Z" +- Desimal: titik sebagai pemisah desimal, misal 125000.50 + +Pagination +---------- +- Gunakan page & per_page +- Meta object pada controller ini biasanya minimal: { total } + +Response Error Umum +------------------- +- 400 Bad Request - payload tidak valid + { + "success": false, + "error": "invalid_request", + "message": "Deskripsi kesalahan", + "details": { "field": ["pesan validasi"] } + } +- 401 Unauthorized - token tidak ada/invalid/expired +- 403 Forbidden - tidak cukup izin +- 404 Not Found +- 429 Too Many Requests - rate limit +- 500 Internal Server Error + +Keamanan dan Persyaratan +------------------------ +- Semua permintaan harus lewat HTTPS (TLS 1.2+) +- Gunakan cookie httpOnly (raylab_jwt / raylab_refresh) untuk flow internal atau header Authorization: Bearer +- Validasi input di server (length, tipe, range) +- Sanitasi data untuk mencegah injection + +Catatan Akhir +------------ +- Spesifikasi ini disesuaikan dengan controller yang ada: /auth, /users, /roles, /permissions dan mekanisme otorisasi berbasis PermissionType. +- Perhatikan bahwa detail field respons (mis. bentuk toResponse pada entitas) dapat berbeda antar resource. Untuk integrasi, gunakan endpoint /auth/me dan /users/me untuk memvalidasi data user yang tersedia. \ No newline at end of file diff --git a/changes.patch b/changes.patch new file mode 100644 index 0000000..bcd6328 Binary files /dev/null and b/changes.patch differ diff --git a/docs/How To Dev.txt b/docs/How To Dev.txt new file mode 100644 index 0000000..68eccff --- /dev/null +++ b/docs/How To Dev.txt @@ -0,0 +1,46 @@ +Note : +- pastikan berada di branch yang benar. +- + +Requirement : +- Node.js 22 LTS + +Step : +1. jalankan "NPM Install". +2. generate prisma client "npx prisma generate". +3. jalankan "cp env.example env" atau buat file .env baru. +4. sesuaikan isi dari .env + +Run Server : +- npm run start:dev + +Command Dev : +- npm test +- npm run lint +- npm run format +- npx prisma studio {untuk cek db} + +Test API : +- menggunakan postman : + arahkan ke file /postman/RayLab.postman_collection.json +- menggunakan swagger : + - masuk ke http://localhost:port/apilist + - klik authorize dan masukkan jwt token + +Controller Requirement : +- diatas deklarasi class controller pastikan ada ini : + - @ApiTags('object') + - @Controller('object') + - @UseGuards(JwtAuthGuard, PermissionGuard) + +- diatas deklarasi method controller pastikan ada ini : + - tipe api : @Post(), @Get(':id'), @Patch(':id'), @Delete(':id') + - @Permissions(PermissionType.SOMETHING) + - @ApiOperation({ summary: 'penjelasan singkat api' }) + +Table : +- untuk table akan dibuat otomatis oleh schema.prisma, disana hanya perlu ditambahkan model atau diubah, jalankan commandnya, setelah itu otomatis tablenya dibuat +- command : npx prisma migrate dev --name {Nama Perubahan} + +Use case: +- penentuan use case ditentukan siapa pemiliknya dahulu, dan harus konsisten. \ No newline at end of file diff --git a/docs/bot-debt-api.md b/docs/bot-debt-api.md new file mode 100644 index 0000000..961007d --- /dev/null +++ b/docs/bot-debt-api.md @@ -0,0 +1,554 @@ +# RayLab Bot Debt API + +Base path: /api/bot/debt/ + +This document is the API contract for the Debt Management endpoints implemented in RayLab Core. The contents are generated from the actual code in the repository and reflect the real request/response shapes and behavior. Do not assume fields or endpoints that are not present in code. + +Authentication +- All endpoints under /api/bot/debt/* require JWT authentication via Authorization: Bearer (JwtAuthGuard). +- CurrentUserGuard maps the verified identity to internal User (via SyncIdentityHandler for external identities). +- All responses are wrapped as { success: true, data: ... } on success. Errors are returned via the global exception filter as { success: false, error: { code, message } }. + +NOTE: See the "Authentication Gap" section below for how Telegram Bots should obtain JWTs. + +--- + +CONTRACT FORMAT (per-endpoint) +Method Path +Authentication +Purpose +Request (example JSON) +Response success (example JSON) +Possible errors (HTTP code + example) +Notes + +--- + +1) POST /api/bot/debt/users/sync +Authentication: Bearer JWT required +Purpose: Sync a Telegram identity to an internal RayLab user via SyncIdentityHandler and return internal user summary. +Request DTO: SyncUserDto +Example request JSON: +{ + "telegramUserId": "123456789", + "displayName": "Rayyan" +} +Response success example: +{ + "success": true, + "data": { + "id": "", + "username": "rayyan", + "name": "rayyan" + } +} +Possible errors: +- 400 Bad Request: { success: false, error: { code: 'BAD_REQUEST', message: 'telegramUserId required' } } +- 401 / 403 may be returned by JwtAuthGuard/CurrentUserGuard depending on authentication/identity validity. +Notes: +- This endpoint uses SyncIdentityHandler under the hood to find/create the internal User based on an external identity with sub `telegram:{telegramUserId}`. +- Calling this endpoint will create or update a User via the project identity patterns. Repeated calls for the same telegramUserId must return the same internal user (no duplicates). +- The endpoint is guarded by JwtAuthGuard + CurrentUserGuard; the caller must present a valid JWT. See "Authentication Gap". + +--- + +2) POST /api/bot/debt/groups +Authentication: Bearer JWT required +Purpose: Create a new Debt Group. The authenticated user becomes the owner. +Request DTO: CreateGroupDto +Example request JSON: +{ "name": "Keluarga" } +Response success example: +{ + "success": true, + "data": { "id": "", "publicId": "RL-1A2B3C", "name": "Keluarga" } +} +Possible errors: +- 400 Bad Request: { success:false, error:{ code:'BAD_REQUEST', message:'name required' } } +- 401 Unauthorized / 403 Forbidden depending on JWT/identity +Notes: +- After create, owner is automatically added as a member. +- publicId is safe to share and used for joining. + +--- + +3) GET /api/bot/debt/groups +Authentication: Bearer JWT required +Purpose: List Debt Groups the current user is a member of. +Request: none +Response success example: +{ + "success": true, + "data": [ + { "id": "", "publicId": "RL-1A2B3C", "name": "Keluarga", "ownerId": "" }, + ... + ] +} +Possible errors: +- 401 Unauthorized +Notes: +- Membership is determined by DebtGroupMember entries. + +--- + +4) GET /api/bot/debt/groups/:id +Authentication: Bearer JWT required +Purpose: Get group detail (people list and basic info). User must be a member. +Request: none +Response success example: +{ + "success": true, + "data": { + "id": "", + "publicId": "RL-1A2B3C", + "name": "Keluarga", + "people": [ { "id": "", "name": "Rayyan" }, ... ] + } +} +Possible errors: +- 404 Not Found: { success:false, error:{ code:'NOT_FOUND', message:'Group not found' } } +- 403 Forbidden: { success:false, error:{ code:'FORBIDDEN', message:'Not a member' } } +Notes: +- Only members can view group detail. + +--- + +5) POST /api/bot/debt/groups/:publicId/join +Authentication: Bearer JWT required +Purpose: Join a group by its publicId (anyone who has publicId can join as member without owner approval). +Request: none +Example request: POST /api/bot/debt/groups/RL-1A2B3C/join +Response success examples: +- On new membership created: +{ + "success": true, + "data": { "id": "" } +} +- If already a member: +{ + "success": true, + "data": { "ok": true } +} +Possible errors: +- 404 Not Found: { success:false, error:{ code:'NOT_FOUND', message:'Group not found' } } +Notes: +- Unique constraint prevents duplicate membership. + +--- + +6) POST /api/bot/debt/groups/:id/leave +Authentication: Bearer JWT required +Purpose: Current user leaves the group. +Request: none +Response success example: +{ "success": true, "data": { "ok": true } } +Possible errors: +- 404 Not Found: Group not found +- 401/403: unauthorized +Notes: +- Implementation deletes DebtGroupMember entries for that (groupId, userId). + +--- + +7) POST /api/bot/debt/groups/:id/people +Authentication: Bearer JWT required (Owner only) +Purpose: Owner adds a Person to the Group. +Request DTO: CreatePersonDto +Example request JSON: +{ "name": "Rayyan" } +Response success example: +{ + "success": true, + "data": { "id": "", "name": "Rayyan" } +} +Possible errors: +- 404 Not Found: Group not found +- 403 Forbidden: { success:false, error:{ code:'FORBIDDEN', message:'Only owner can add person' } } +- 400 Bad Request: { success:false, error:{ code:'BAD_REQUEST', message:'Person with same name already exists in group' } } +Notes: +- Person names are unique per group (DB @@unique([groupId,name])). +- Person != Telegram User (see Person section). + +--- + +8) GET /api/bot/debt/groups/:id/people +Authentication: Bearer JWT required +Purpose: List People inside a group (member-only). +Request: none +Response success example: +{ + "success": true, + "data": [ { "id": "", "name": "Rayyan" }, ... ] +} +Possible errors: +- 403 Forbidden: Not a member +Notes: +- Only active (isDeleted=false) persons are returned. + +--- + +9) DELETE /api/bot/debt/groups/:id/people/:personId +Authentication: Bearer JWT required (Owner only) +Purpose: Soft-delete a Person from the Group. +Request: none +Response success example: +{ "success": true, "data": null } +Possible errors: +- 404 Not Found: Group not found +- 403 Forbidden: Only owner can remove person +Notes: +- Implementation sets isDeleted=true (soft delete). Historical transactions remain and are not removed. +- After soft-delete this Person cannot be used for new transactions. + +--- + +10) POST /api/bot/debt/groups/:id/transactions/debt +Authentication: Bearer JWT required (Member) +Purpose: Create a DEBT transaction (ledger entry). +Request DTO: CreateTransactionDto (example): +{ + "from": "Rayyan", + "to": "Krisda", + "price": "17.000", + "description": "Makan siang", + "date": "2026/09/01", // optional, format yyyy/mm/dd + "requestId": "telegram-update-12345" +} +Response success example (created DEBT transaction model as returned by Prisma, wrapped): +{ + "success": true, + "data": { + "id": "", + "groupId": "", + "fromPersonId": "", + "toPersonId": "", + "amount": 17000, + "type": "DEBT", + "description": "Makan siang", + "transactionDate": "2026-09-01T00:00:00.000Z", + "createdAt": "2026-09-01T10:00:00.000Z", + "createdByUserId": "", + "requestId": "telegram-update-12345" + } +} +Possible errors: +- 400 Bad Request: + - "from and to required" + - "from and to cannot be same" + - "Person not found in group" + - "requestId is required for idempotency" + - "Nominal tidak valid. Gunakan format seperti 17000 atau 17.000." + - "Nominal harus lebih besar dari 0" + - "Tanggal tidak valid. Gunakan format yyyy/mm/dd" +- 403 Forbidden: Not a member +- 404 Not Found: Group not found +Notes: +- requestId is required by service to ensure idempotency; DB also has unique(groupId, requestId) as last-resort protection. +- price is a string in request; service parses dot separators and stores BigInt. +- transactionDate stored as date-only (Asia/Jakarta by default if omitted). + +--- + +11) POST /api/bot/debt/groups/:id/transactions/payment +Authentication: Bearer JWT required (Member) +Purpose: Create a PAYMENT ledger entry. If payment exceeds existing debt, a reverse DEBT transaction is created automatically for the remainder. +Request DTO: CreateTransactionDto (same shape as DEBT) +Example request JSON: +{ + "from": "Rayyan", + "to": "Krisda", + "price": "60.000", + "description": "Bayar lebih", + "requestId": "telegram-update-67890" +} +Response success examples: +- If payment does not produce remainder (no overpayment): +{ + "success": true, + "data": { /* PAYMENT transaction object like DEBT example */ } +} +- If payment > existing debt and remainder produced: +{ + "success": true, + "data": { + "payment": { /* payment transaction object */ }, + "remainder": { /* created DEBT transaction reversed, i.e. to->from */ } + } +} +Possible errors: same as DEBT endpoint (validation, membership, group not found) +Notes: +- Behavior: create PAYMENT ledger entry; compute existing net between the two persons (aggregating ledger), then if payment > existing positive debt, create a new DEBT transaction in opposite direction for remainder with description: "Sisa pembayaran hutang dari {creatorName}". +- The ledger is immutable: PAYMENT and resultant DEBT(reverse) are separate records. +- Avoid double-counting: the net is computed from ledger BEFORE the new PAYMENT; after PAYMENT + possible reverse DEBT, aggregate net will reflect correct result. + +--- + +12) GET /api/bot/debt/groups/:id/transactions +Authentication: Bearer JWT required (Member) +Purpose: Retrieve raw ledger history for group. +Query params: page (optional), limit (optional) — current implementation ignores advanced paging; controller accepts page & limit but service returns all transactions for group. +Response success example: +{ + "success": true, + "data": [ + { "id": "", "from": "Rayyan", "to": "Krisda", "amount": "17000", "type": "DEBT", "description": "Makan", "transactionDate": "2026-09-01", "createdAt": "2026-09-01T10:00:00Z" }, + ... + ] +} +Possible errors: +- 403 Forbidden: Not a member +Notes: +- Results are ordered by transactionDate ASC, then createdAt ASC to ensure deterministic ordering. +- This endpoint is RAW ledger history (detail), not summary. + +--- + +13) GET /api/bot/debt/groups/:id/summary +Authentication: Bearer JWT required (Member) +Purpose: Compute CURRENT NET DEBT for the group using global netting (read-only). The algorithm aggregates ledger and performs deterministic greedy settlement. It does not change the ledger. +Response success example: +{ + "success": true, + "data": [ + { "from": "Rayyan", "to": "Krisda", "amount": "22000" }, + ... + ] +} +If no debts remain: +{ + "success": true, + "data": [] +} +Possible errors: +- 403 Forbidden: Not a member +Notes: +- Summary output is a settlement list: who should pay whom and how much, computed by the server's netting algorithm. +- Different settlement variants may be valid; the server returns one deterministic settlement (greedy algorithm). + +--- + +14) GET /api/bot/debt/groups/:id/detail +Authentication: Bearer JWT required (Member) +Purpose: Same as GET /transactions — return RAW LEDGER HISTORY ordered by transactionDate ASC, createdAt ASC. +Response: same as /transactions endpoint. +Notes: For Telegram commands, /TampilkanDetailHutang should call this endpoint. + +--- + +ERROR MAPPING +RayLab Core uses a global exception filter which returns errors as: +{ + "success": false, + "error": { "code": "", "message": "" } +} + +Common error cases (HTTP status and example response): +- 400 Bad Request + - Example: invalid amount + - {"success": false, "error": {"code":"BAD_REQUEST","message":"Nominal tidak valid. Gunakan format seperti 17000 atau 17.000."}} +- 403 Forbidden + - Example: not a member or not owner + - {"success": false, "error": {"code":"FORBIDDEN","message":"Not a member"}} +- 404 Not Found + - Example: Group not found + - {"success": false, "error": {"code":"NOT_FOUND","message":"Group not found"}} +- 500 Internal Error: unexpected exceptions + +Exact messages returned are the exception.message strings thrown by the service (see service code for exact strings). + +--- + +TELEGRAM BOT INTEGRATION (GUIDE) +Runtime flow (recommended): +Telegram User -> Telegram Bot -> RayLab Core (/api/bot/debt) with JWT auth + +Key points: +- Each API call requires JWT authentication (JwtAuthGuard). The CurrentUserGuard maps the JWT-validated identity to internal User. +- The codebase does not provide an automatic JWT issuance endpoint for Telegram Bot on behalf of a Telegram user. Auth flow is based on existing RayLab authentication (Authentik / OIDC / internal JWT generation). + +Recommended integration options for Telegram Bot developers (choose one depending on security & user experience): +1) Per-user interactive login (most secure): + - The Telegram Bot prompts the user to authenticate via the RayLab web login (OIDC) and obtain a RayLab internal access token. The bot instructs user to paste a short-lived token or do a one-time link flow. + - Bot uses that token to call /api/bot/debt with Authorization: Bearer . + - Pros: actions executed under user identity; createdByUserId matches the real user. + - Cons: requires user interaction and web flow. + +2) Service-account with explicit recorded actor (less ideal): + - Bot uses a service account JWT to call API. createdByUserId will be service-account id. + - To preserve traceability, Bot includes requestId and includes in request.body metadata about the Telegram user (but server currently does not accept/verify a claimed telegramUserId). This is less secure because createdByUserId won't match Telegram user. + +3) Implement server-side mapping endpoint (recommended by platform team): + - Add a secure server endpoint that accepts a Telegram update signed or validated by bot token and exchanges it for a user-specific JWT using SyncIdentityHandler and internal authentication service. + - This requires changes outside the current feature and must follow security review. + +Authentication Gap (explicit): +- The repository currently expects callers to present JWTs. There is no built-in endpoint to exchange a Telegram user_id for a JWT without user authentication. +- As a result, Telegram Bot developers must either obtain per-user JWTs via the normal login flow, or use a service account JWT (with tradeoffs), or request project owners to implement a secure bot-to-server auth bridge. + +When to call /users/sync? +- /users/sync maps a Telegram external identity into internal User via SyncIdentityHandler. It should be called when you need the internal user to exist or to refresh data. If you follow per-user login (option 1), explicit /users/sync may not be needed. If you use a server-side bridging approach, call /users/sync as part of the bridging sequence to ensure the internal user exists. +- Do not call /users/sync unauthenticated. The endpoint is guarded and requires a JWT. + +--- + +TELEGRAM COMMAND → API MAPPING (examples) +1) Create Group +Telegram: /BuatGroup Keluarga +Bot: POST /api/bot/debt/groups +Request body: { "name": "Keluarga" } +Response: returns group publicId (e.g. RL-1A2B3C) -> Bot shares this with group members to join. + +2) Join Group +Telegram: /MasukGroup RL-1A2B3C +Bot: POST /api/bot/debt/groups/RL-1A2B3C/join + +3) Add Person +Telegram: /TambahOrang Rayyan +Bot: POST /api/bot/debt/groups/:id/people +Body: { "name": "Rayyan" } +Note: only owner can add person (owner determined by JWT caller). + +4) Add Debt +Telegram: /TambahHutang Rayyan - Krisda - 17.000 - Makan siang +Bot: POST /api/bot/debt/groups/:id/transactions/debt +Body: +{ + "from": "Rayyan", + "to": "Krisda", + "price": "17.000", + "description": "Makan siang", + "requestId": "telegram-update-12345" +} +If date provided (yyyy/mm/dd) include "date" field. + +5) Payment +Telegram: /BayarHutang Rayyan Krisda 10.000 +Bot: POST /api/bot/debt/groups/:id/transactions/payment +Body: +{ + "from": "Rayyan", + "to": "Krisda", + "price": "10.000", + "requestId": "telegram-update-67890" +} + +6) Detail +Telegram: /TampilkanDetailHutang +Bot: GET /api/bot/debt/groups/:id/detail +Bot shows raw ledger lines as returned. + +7) Summary +Telegram: /TampilkanKesimpulanHutang +Bot: GET /api/bot/debt/groups/:id/summary +Bot shows summarized settlement lines. + +--- + +EXAMPLE END-TO-END FLOW (minimal) +1) Owner Alice authenticates via RayLab OIDC, obtains JWT (out of scope for bot automation). Bot now holds Alice's JWT for use. +2) Alice: /BuatGroup Keluarga + Bot: POST /api/bot/debt/groups with Alice's JWT -> receives { publicId: "RL-1A2B3C" } + Bot replies: "Group created. Public ID RL-1A2B3C" +3) Bob obtains publicId and calls /MasukGroup RL-1A2B3C (after Bob has a JWT) + Bot: POST /api/bot/debt/groups/RL-1A2B3C/join with Bob's JWT +4) Alice (owner) adds people: + /TambahOrang Rayyan -> POST /api/bot/debt/groups/:id/people { name: 'Rayyan' } + /TambahOrang Krisda -> POST /... { name: 'Krisda' } +5) Bob (member) adds debt: + /TambahHutang Rayyan - Krisda - 17000 - Makan siang -> POST transactions/debt with his JWT and requestId based on message id +6) Someone asks /TampilkanKesimpulanHutang -> GET summary -> Bot displays settlement +7) Someone asks /TampilkanDetailHutang -> GET detail -> Bot displays ledger lines + +--- + +IDEMPOTENCY +- requestId in CreateTransactionDto is required by service. The DB has a unique constraint @@unique([groupId, requestId]). +- Behavior: + - If a request with the same (groupId, requestId) already exists, service returns the existing transaction and does not create a duplicate. + - In concurrent scenarios, DB unique constraint is the final line of defense; clients should retry on unique-violation errors by re-fetching the existing transaction or returning success. + +Recommendation for bot developers: +- Use a stable requestId per Telegram update (e.g., `telegram:update:{update_id}` or `telegram:message:{chat_id}:{message_id}`) to allow safe retries. + +--- + +SECURITY REVIEW (quick) +1) JWT validation + - JwtAuthGuard verifies external JWKS (Authentik) or internal secret. Use Authorization: Bearer . +2) Identity spoofing + - /users/sync is guarded by JwtAuthGuard; unauthorized clients cannot arbitrarily create arbitrary internal users. + - However, there is no remote-check confirming that the JWT presented *belongs to* the telegramUserId included in /users/sync body. The server uses SyncIdentityHandler to map identity from verified claims when CurrentUserGuard is used; but the sync endpoint currently expects authenticated callers. If a bot uses a service account JWT, it can call /users/sync for any telegramUserId — the system will create internal users based on given sub `telegram:{id}`. This is a gap: a bot acting on behalf of a Telegram user must obtain a JWT THAT represents that user, or an auth bridge must be implemented. +3) Group membership and owner + - Owner checks compare req.currentUser.id to group.ownerId (server-side). This is enforced server-side. +4) Person name manipulation + - Person lookup in createTransaction is by name within groupId and isDeleted=false. Be careful: names are the identity used in the API; bots must ensure proper escaping and normalization when parsing from Telegram. +5) requestId manipulation + - requestId is honored by server and unique constraint used; ensure bots generate collision-resistant ids. +6) DTO whitelist + - Global ValidationPipe is enabled (whitelist:true, transform:true, forbidNonWhitelisted:true) so unexpected fields are rejected. +7) SQL/Prisma injection + - Prisma queries use parameterized API. However, person lookup is by name; ensure the bot sends sanitized strings. Prisma prevents SQL injection when using its query methods. +8) Deleted Person + - Soft-deleted Person cannot be used for new transactions; queries use isDeleted=false. Historical transactions remain readable. + +Security finding / gap (explicit): +- There is no built-in server-side flow that allows the Telegram Bot to obtain a JWT on behalf of a Telegram user without user interaction. This means a bot cannot impersonate the real user securely unless that user performs the standard authentication flow (OIDC). If you plan to let Bot perform actions as real users without user login, you must implement a secure delegation mechanism (requires architectural review). + +--- + +SWAGGER +- RayLab Core includes Swagger support (if SWAGGER_ENABLED=true in config, docs available). The debt controller classes use DTOs; if Swagger is enabled the endpoints will appear. The project already includes @nestjs/swagger — enabling will include these endpoints. + +--- + +TESTS / BUILD / MIGRATION STATUS +- Unit tests (run locally during development): all unit tests in this repo passed in this environment. + - Test suites run: 10 + - Tests: 29 passed +- Build: tsc compile succeeded in this environment. +- Prisma generate: succeeded (Prisma Client generated locally). +- Prisma migrate dev --name add_debt_models: NOT APPLIED in this environment due to unreachable DB (P1001: Can't reach database server at `postgres-raylab:5432`). + - Action required: run migration in environment with reachable DB; do not run migrate reset in production. + +--- + +FILES CHANGED (by implementation) +- prisma/schema.prisma (added debt models + backrefs) +- src/modules/bot-debt/debt.module.ts +- src/modules/bot-debt/presentation/debt.controller.ts +- src/modules/bot-debt/presentation/dto/sync-user.dto.ts +- src/modules/bot-debt/presentation/dto/create-group.dto.ts +- src/modules/bot-debt/presentation/dto/create-person.dto.ts +- src/modules/bot-debt/presentation/dto/create-transaction.dto.ts +- src/modules/bot-debt/application/debt.service.ts +- src/app.module.ts +- tests/unit/debt.service.spec.ts + +--- + +REMAINING ISSUES / ACTIONS FOR OPERATIONAL TEAM +1) Apply Prisma migration in environment where database is reachable: + - npx prisma generate + - npx prisma migrate dev --name add_debt_models + - Commit the generated migration directory if your process requires it. +2) Decide on authentication flow for Telegram Bot: + - Option A (recommended): Bot guides users to obtain per-user JWT via OIDC login; Bot uses users' JWT to call API. + - Option B: Implement a secure server-side exchange for Telegram identities to internal JWTs (requires new endpoint & security review). +3) Optionally make requestId DB column NOT NULL to enforce client discipline. (Current code requires requestId in service; DB column is nullable — left intentionally to maintain compatibility.) +4) Add integration tests (optional): minimal end-to-end test that runs migrate + spins up app and exercises endpoints. + +--- + +SUMMARY STATEMENT +This document is the canonical API contract for the Debt Management endpoints implemented at /api/bot/debt/. It contains concrete request & response JSON, error mapping, identity & integration guidance for Telegram Bot implementers, and notes about security and idempotency. + +Next steps for Bot developers: +- Decide how your bot will obtain JWTs (per-user or service-account). If per-user, implement a login flow; if service-account, accept createdByUserId will be the service account. +- Implement stable requestId generation per Telegram update to ensure idempotency. +- Use DTO field names exactly as documented above. + +If you want, I can now: +- Add explicit Swagger decorators/examples to the controllers (so generated API docs include exact DTOs and examples), and +- Add a small integration test (if you can provide reachable test DB credentials or run the migration locally and provide migration status). + + diff --git a/jest.config.cjs b/jest.config.cjs index 5cdaf29..1a8ec21 100644 --- a/jest.config.cjs +++ b/jest.config.cjs @@ -1,6 +1,13 @@ module.exports = { preset: 'ts-jest', testEnvironment: 'node', - testMatch: ['**/?(*.)+(spec|test).[tj]s?(x)'], + testMatch: ['**/tests/**/*.spec.ts', '**/?(*.)+(spec|test).[tj]s?(x)'], + testPathIgnorePatterns: ['/tests/api/', '/tests/integration/'], moduleFileExtensions: ['ts', 'js', 'json', 'node'], + globals: { + 'ts-jest': { + tsconfig: 'tsconfig.json', + }, + }, }; + diff --git a/my-project.bundle b/my-project.bundle new file mode 100644 index 0000000..0d4559c Binary files /dev/null and b/my-project.bundle differ diff --git a/package-lock.json b/package-lock.json index 1056868..8a7bebd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,32 +1,409 @@ { "name": "raylab-core", "version": "0.1.0", - "lockfileVersion": 1, + "lockfileVersion": 3, "requires": true, - "dependencies": { - "@babel/code-frame": { + "packages": { + "": { + "name": "raylab-core", + "version": "0.1.0", + "license": "MIT", + "dependencies": { + "@aws-sdk/client-s3": "^3.379.0", + "@nestjs/common": "^10.4.20", + "@nestjs/config": "^3.3.0", + "@nestjs/core": "^10.4.20", + "@nestjs/jwt": "^10.2.0", + "@nestjs/mapped-types": "^2.1.0", + "@nestjs/passport": "^10.0.3", + "@nestjs/platform-express": "^10.4.20", + "@nestjs/swagger": "^7.4.2", + "@prisma/client": "^5.22.0", + "axios": "^1.4.0", + "bcrypt": "^5.1.1", + "bullmq": "^1.73.0", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.2", + "cookie-parser": "^1.4.7", + "dotenv": "^16.6.1", + "ioredis": "^5.3.2", + "jose": "^6.2.6", + "jsonwebtoken": "^9.0.2", + "nodemailer": "^6.9.4", + "openid-client": "^6.5.0", + "passport": "^0.7.0", + "passport-jwt": "^4.0.1", + "pino": "^9.7.0", + "reflect-metadata": "^0.2.2", + "rxjs": "^7.8.2", + "swagger-ui-express": "^5.0.1", + "uuid": "^11.1.0" + }, + "devDependencies": { + "@eslint/js": "^9.21.0", + "@nestjs/testing": "^10.4.22", + "@playwright/test": "^1.62.1", + "@types/bcrypt": "^5.0.2", + "@types/cookie-parser": "^1.4.10", + "@types/jest": "^29.5.14", + "@types/node": "^22.20.1", + "@types/passport-jwt": "^4.0.1", + "@types/supertest": "^6.0.3", + "@types/swagger-ui-express": "^4.1.8", + "@typescript-eslint/eslint-plugin": "^8.25.0", + "@typescript-eslint/parser": "^8.25.0", + "eslint": "^9.21.0", + "globals": "^16.0.0", + "jest": "^29.7.0", + "prettier": "^3.5.3", + "prisma": "^5.22.0", + "supertest": "^7.0.0", + "ts-jest": "^29.2.6", + "ts-node": "^10.9.2", + "ts-node-dev": "^2.0.0", + "typescript": "^5.9.3" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/checksums": { + "version": "3.1000.24", + "resolved": "https://registry.npmjs.org/@aws-sdk/checksums/-/checksums-3.1000.24.tgz", + "integrity": "sha512-7TWLjypP8kk3savsDBRuhZJx7mBuFFA2136BQhwwLllsAnO4Tmq/p+SXZaNxbuulkzUFz3BZzj0bb4YzexZcNQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.977.4", + "@aws-sdk/types": "^3.974.2", + "@smithy/core": "^3.31.1", + "@smithy/types": "^4.16.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-s3": { + "version": "3.1101.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.1101.0.tgz", + "integrity": "sha512-16EFb1aTEBgPcfUAWAjjlB57IZCyn7B3rlfT+xqE7M6WoH8AMMU3vFZO0UOitwh/xvvzVx73YED1/n0PU4qBMw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/checksums": "^3.1000.24", + "@aws-sdk/core": "^3.977.4", + "@aws-sdk/credential-provider-node": "^3.972.76", + "@aws-sdk/middleware-sdk-s3": "^3.972.70", + "@aws-sdk/signature-v4-multi-region": "^3.996.43", + "@aws-sdk/types": "^3.974.2", + "@smithy/core": "^3.31.1", + "@smithy/fetch-http-handler": "^5.6.13", + "@smithy/node-http-handler": "^4.9.13", + "@smithy/types": "^4.16.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/core": { + "version": "3.977.4", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.977.4.tgz", + "integrity": "sha512-CEkcQlMOQJCvul60U7wdAOACjtdgFWDsfJI+6wUOGdhGNV2lGbuJpi/R50QLpFG3Tp+sQxa/RmzC3X7KHbhuTA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.974.2", + "@aws-sdk/xml-builder": "^3.972.37", + "@aws/lambda-invoke-store": "^0.3.0", + "@smithy/core": "^3.31.1", + "@smithy/signature-v4": "^5.6.12", + "@smithy/types": "^4.16.1", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-env": { + "version": "3.972.65", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.65.tgz", + "integrity": "sha512-lJT2aRw9wCV8jPHyFJjdZLD4HTydL6/22AnCSOB8e/LqOc55nEJGLHkJQeSxhn8QiqyjFwPKQFtMw0ovjRUY/g==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.977.4", + "@aws-sdk/types": "^3.974.2", + "@smithy/core": "^3.31.1", + "@smithy/types": "^4.16.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-http": { + "version": "3.972.67", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.67.tgz", + "integrity": "sha512-N7fw/15hSwI/CPxe5ohOyb7O4ge9f5me1gVIn8OIkBRB0squ8OJqQyDyH/HoL+Sb1W5xdC88jVC+bHkw73iu+Q==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.977.4", + "@aws-sdk/types": "^3.974.2", + "@smithy/core": "^3.31.1", + "@smithy/fetch-http-handler": "^5.6.13", + "@smithy/node-http-handler": "^4.9.13", + "@smithy/types": "^4.16.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.973.10", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.973.10.tgz", + "integrity": "sha512-Zh9XRaPnDN9buO7GfWBubS22R6Nq5D6hbyYEMN05LiOnXugm/8WDjUx6y756bSPbdn3aJB2qG4zFW3bN82QhoQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.977.4", + "@aws-sdk/credential-provider-env": "^3.972.65", + "@aws-sdk/credential-provider-http": "^3.972.67", + "@aws-sdk/credential-provider-login": "^3.972.72", + "@aws-sdk/credential-provider-process": "^3.972.65", + "@aws-sdk/credential-provider-sso": "^3.973.9", + "@aws-sdk/credential-provider-web-identity": "^3.972.71", + "@aws-sdk/nested-clients": "^3.997.39", + "@aws-sdk/types": "^3.974.2", + "@smithy/core": "^3.31.1", + "@smithy/credential-provider-imds": "^4.4.16", + "@smithy/types": "^4.16.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-login": { + "version": "3.972.72", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.72.tgz", + "integrity": "sha512-zZapIKwaHp7TdTf9hbH1I3CVUdEupmt7FXO/BoTQGC+4h6NkXKWpqF2p5WyfpjurDLHCpSyh+BzMlAg8arqWLA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.977.4", + "@aws-sdk/nested-clients": "^3.997.39", + "@aws-sdk/types": "^3.974.2", + "@smithy/core": "^3.31.1", + "@smithy/types": "^4.16.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-node": { + "version": "3.972.76", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.76.tgz", + "integrity": "sha512-1yzLmRiYSgGC25v7ZZEwJn/auhHHTIHgFOmzL2f36hf1+7jSLcX+1QrAz4760WEzPiiQl8xmlpFhHfl2OoyVzA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "^3.972.65", + "@aws-sdk/credential-provider-http": "^3.972.67", + "@aws-sdk/credential-provider-ini": "^3.973.10", + "@aws-sdk/credential-provider-process": "^3.972.65", + "@aws-sdk/credential-provider-sso": "^3.973.9", + "@aws-sdk/credential-provider-web-identity": "^3.972.71", + "@aws-sdk/types": "^3.974.2", + "@smithy/core": "^3.31.1", + "@smithy/credential-provider-imds": "^4.4.16", + "@smithy/types": "^4.16.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-process": { + "version": "3.972.65", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.65.tgz", + "integrity": "sha512-e5DbbNteOSalN58U83G6kFa4ECLEuGbGqNBHIXE7zYXA/m4GHblIGjFbSH7wYv6gBV8iNSDcRZBKfQZF5vF9nw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.977.4", + "@aws-sdk/types": "^3.974.2", + "@smithy/core": "^3.31.1", + "@smithy/types": "^4.16.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.973.9", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.973.9.tgz", + "integrity": "sha512-0V0u4t+KBku9fbh5CPCaC5hUWwSzDafp8nCuDy817zWbp2gz80jO44rMQkiwnZ+k54B+tjAtzRy00DJRGTKGBg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.977.4", + "@aws-sdk/nested-clients": "^3.997.39", + "@aws-sdk/token-providers": "3.1100.0", + "@aws-sdk/types": "^3.974.2", + "@smithy/core": "^3.31.1", + "@smithy/types": "^4.16.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.972.71", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.71.tgz", + "integrity": "sha512-e4dwiRltGAaQ+2yxw57Hj0l/BF3BHiG14+QpYE7bGYBlpAq/fkIri2BDhjWon8c0mhhtd2txQBAkQb9BcTStFg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.977.4", + "@aws-sdk/nested-clients": "^3.997.39", + "@aws-sdk/types": "^3.974.2", + "@smithy/core": "^3.31.1", + "@smithy/types": "^4.16.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-sdk-s3": { + "version": "3.972.70", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.972.70.tgz", + "integrity": "sha512-APdP0iODt39AkjCjzTFIoFrxDH/Cz3CpWRDKLcsJg7eOnfE1htkxL9BhDoe/xL7cXdoMwh2HBYv3DiT1uf64NQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.977.4", + "@aws-sdk/signature-v4-multi-region": "^3.996.43", + "@aws-sdk/types": "^3.974.2", + "@smithy/core": "^3.31.1", + "@smithy/types": "^4.16.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/nested-clients": { + "version": "3.997.39", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.39.tgz", + "integrity": "sha512-wU5NPnj62Sb7A8xn/Zb+xThe05P3otNtDl37iOIi5DDMeCesNeCckaG+eXWGUs12Z9R34I8CD05TaTe6SIa61g==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.977.4", + "@aws-sdk/signature-v4-multi-region": "^3.996.43", + "@aws-sdk/types": "^3.974.2", + "@smithy/core": "^3.31.1", + "@smithy/fetch-http-handler": "^5.6.13", + "@smithy/node-http-handler": "^4.9.13", + "@smithy/types": "^4.16.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/signature-v4-multi-region": { + "version": "3.996.43", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.43.tgz", + "integrity": "sha512-lKekx8bLBXSv4O+cslk9Zfnw2XKSkWBs3uWL5QGhH2ZAQfNS7FE0vcSSN2vD/AhxX54ZTywWxR4STThoeOXlBA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.974.2", + "@smithy/signature-v4": "^5.6.12", + "@smithy/types": "^4.16.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/token-providers": { + "version": "3.1100.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1100.0.tgz", + "integrity": "sha512-THf3MkgY3fNJZ3zdgSenLqR7gSE68KccCj1RCKretlG73Ppszvues02VpCUO9NlB/tZDC483FvGCld+AiPCkvg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.977.4", + "@aws-sdk/nested-clients": "^3.997.39", + "@aws-sdk/types": "^3.974.2", + "@smithy/core": "^3.31.1", + "@smithy/types": "^4.16.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/types": { + "version": "3.974.2", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.974.2.tgz", + "integrity": "sha512-3W6IUtSxFbH6X7Wb7DzGCV5QiFQsd0g8bOfntpmDxQlzBoKWUMBu/JPQR0DwkE+Hpnxd6db1tXbOwdeHddG6cA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.16.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/xml-builder": { + "version": "3.972.37", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.37.tgz", + "integrity": "sha512-zKq4HQum8JwDyEuyfuI4bbiAcU0KxP6qy+9PR/IsR92IyE/DaBAikzAS50tjxip4bqIIANpCcG+Yyj6CVhXupg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.16.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws/lambda-invoke-store": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.3.0.tgz", + "integrity": "sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0" + } + }, + "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, - "requires": { + "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" } }, - "@babel/compat-data": { + "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 + "dev": true, + "engines": { + "node": ">=6.9.0" + } }, - "@babel/core": { + "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, - "requires": { + "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-compilation-targets": "^7.29.7", @@ -43,289 +420,409 @@ "json5": "^2.2.3", "semver": "^6.3.1" }, - "dependencies": { - "semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true - } + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" } }, - "@babel/generator": { + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "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, - "requires": { + "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" } }, - "@babel/helper-compilation-targets": { + "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, - "requires": { + "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" }, - "dependencies": { - "semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true - } + "engines": { + "node": ">=6.9.0" } }, - "@babel/helper-globals": { + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-globals": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", - "dev": true + "dev": true, + "engines": { + "node": ">=6.9.0" + } }, - "@babel/helper-module-imports": { + "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, - "requires": { + "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" } }, - "@babel/helper-module-transforms": { + "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, - "requires": { + "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" } }, - "@babel/helper-plugin-utils": { + "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 + "dev": true, + "engines": { + "node": ">=6.9.0" + } }, - "@babel/helper-string-parser": { + "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 + "dev": true, + "engines": { + "node": ">=6.9.0" + } }, - "@babel/helper-validator-identifier": { + "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 + "dev": true, + "engines": { + "node": ">=6.9.0" + } }, - "@babel/helper-validator-option": { + "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 + "dev": true, + "engines": { + "node": ">=6.9.0" + } }, - "@babel/helpers": { + "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, - "requires": { + "dependencies": { "@babel/template": "^7.29.7", "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" } }, - "@babel/parser": { + "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, - "requires": { + "dependencies": { "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" } }, - "@babel/plugin-syntax-async-generators": { + "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, - "requires": { + "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "@babel/plugin-syntax-bigint": { + "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, - "requires": { + "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "@babel/plugin-syntax-class-properties": { + "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, - "requires": { + "dependencies": { "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "@babel/plugin-syntax-class-static-block": { + "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, - "requires": { + "dependencies": { "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "@babel/plugin-syntax-import-attributes": { + "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, - "requires": { + "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "@babel/plugin-syntax-import-meta": { + "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, - "requires": { + "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "@babel/plugin-syntax-json-strings": { + "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, - "requires": { + "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "@babel/plugin-syntax-jsx": { + "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, - "requires": { + "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "@babel/plugin-syntax-logical-assignment-operators": { + "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, - "requires": { + "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "@babel/plugin-syntax-nullish-coalescing-operator": { + "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, - "requires": { + "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "@babel/plugin-syntax-numeric-separator": { + "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, - "requires": { + "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "@babel/plugin-syntax-object-rest-spread": { + "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, - "requires": { + "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "@babel/plugin-syntax-optional-catch-binding": { + "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, - "requires": { + "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "@babel/plugin-syntax-optional-chaining": { + "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, - "requires": { + "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "@babel/plugin-syntax-private-property-in-object": { + "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, - "requires": { + "dependencies": { "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "@babel/plugin-syntax-top-level-await": { + "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, - "requires": { + "dependencies": { "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "@babel/plugin-syntax-typescript": { + "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, - "requires": { + "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "@babel/template": { + "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, - "requires": { + "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" } }, - "@babel/traverse": { + "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, - "requires": { + "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-globals": "^7.29.7", @@ -333,108 +830,144 @@ "@babel/template": "^7.29.7", "@babel/types": "^7.29.7", "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" } }, - "@babel/types": { + "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, - "requires": { + "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" } }, - "@bcoe/v8-coverage": { + "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 }, - "@borewit/text-codec": { + "node_modules/@borewit/text-codec": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/@borewit/text-codec/-/text-codec-0.2.2.tgz", - "integrity": "sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ==" + "integrity": "sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } }, - "@cspotcode/source-map-support": { + "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, - "requires": { + "dependencies": { "@jridgewell/trace-mapping": "0.3.9" }, - "dependencies": { - "@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, - "requires": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" - } - } + "engines": { + "node": ">=12" } }, - "@eslint-community/eslint-utils": { + "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, + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@eslint-community/eslint-utils": { "version": "4.10.1", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", "dev": true, - "requires": { + "dependencies": { "eslint-visitor-keys": "^3.4.3" }, - "dependencies": { - "eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true - } + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, - "@eslint-community/regexpp": { + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { "version": "4.12.2", "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", - "dev": true + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } }, - "@eslint/config-array": { + "node_modules/@eslint/config-array": { "version": "0.21.2", "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", "dev": true, - "requires": { + "dependencies": { "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", "minimatch": "^3.1.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "@eslint/config-helpers": { + "node_modules/@eslint/config-helpers": { "version": "0.4.2", "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", "dev": true, - "requires": { + "dependencies": { "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "@eslint/core": { + "node_modules/@eslint/core": { "version": "0.17.0", "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", "dev": true, - "requires": { + "dependencies": { "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "@eslint/eslintrc": { + "node_modules/@eslint/eslintrc": { "version": "3.3.6", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.6.tgz", "integrity": "sha512-l2Ul9PrHsPCKcEY/ac7VgFj9D80C7S68sOKc618SyHDPK36s1XcFebXY0iTzUVn4Yq+YbwvSnDmCz9yxjX+QrA==", "dev": true, - "requires": { + "dependencies": { "ajv": "^6.14.0", "debug": "^4.3.2", "espree": "^10.0.1", @@ -445,193 +978,288 @@ "minimatch": "^3.1.5", "strip-json-comments": "^3.1.1" }, - "dependencies": { - "globals": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", - "dev": true - }, - "ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true - }, - "js-yaml": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", - "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", - "dev": true, - "requires": { - "argparse": "^2.0.1" - } - } + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, - "@eslint/js": { + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/eslintrc/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/@eslint/eslintrc/node_modules/js-yaml": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", + "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@eslint/js": { "version": "9.39.5", "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.5.tgz", "integrity": "sha512-QywQuszQh77pIXCsq998c8hbhSTI/azTty1Z6N53dmAudKHhy573j3yvRLsX2BSp8YpLtoCEG8E9DJe+8zUh4A==", - "dev": true + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } }, - "@eslint/object-schema": { + "node_modules/@eslint/object-schema": { "version": "2.1.7", "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", - "dev": true + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } }, - "@eslint/plugin-kit": { + "node_modules/@eslint/plugin-kit": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", "dev": true, - "requires": { + "dependencies": { "@eslint/core": "^0.17.0", "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "@humanfs/core": { + "node_modules/@humanfs/core": { "version": "0.19.2", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", "dev": true, - "requires": { + "dependencies": { "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" } }, - "@humanfs/node": { + "node_modules/@humanfs/node": { "version": "0.16.8", "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", "dev": true, - "requires": { + "dependencies": { "@humanfs/core": "^0.19.2", "@humanfs/types": "^0.15.0", "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" } }, - "@humanfs/types": { + "node_modules/@humanfs/types": { "version": "0.15.0", "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", - "dev": true + "dev": true, + "engines": { + "node": ">=18.18.0" + } }, - "@humanwhocodes/module-importer": { + "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } }, - "@humanwhocodes/retry": { + "node_modules/@humanwhocodes/retry": { "version": "0.4.3", "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", - "dev": true + "dev": true, + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } }, - "@istanbuljs/load-nyc-config": { + "node_modules/@ioredis/commands": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.10.0.tgz", + "integrity": "sha512-UmeW7z4LfctwoQ5wkhVzgq8tXkreED2xZGpX+Bg+zA+WJFZCT6c062AfCK/Dfk81xZnnwdhJCUMkitihRaoC2Q==", + "license": "MIT" + }, + "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, - "requires": { + "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" }, - "dependencies": { - "argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, - "requires": { - "sprintf-js": "~1.0.2" - } - }, - "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, - "requires": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - } - }, - "js-yaml": { - "version": "3.15.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.0.tgz", - "integrity": "sha512-ttBQIIQPDeLjpPOohtUdXuXUVoA2uIB6fEH9HyJ7234s5mBJ5wTx20njxplLZQgLaOfpmPQA7X2t5AX6tIPbog==", - "dev": true, - "requires": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - } - }, - "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, - "requires": { - "p-locate": "^4.1.0" - } - }, - "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, - "requires": { - "p-try": "^2.0.0" - } - }, - "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, - "requires": { - "p-limit": "^2.2.0" - } - }, - "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 - } + "engines": { + "node": ">=8" } }, - "@istanbuljs/schema": { + "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.0.tgz", + "integrity": "sha512-ttBQIIQPDeLjpPOohtUdXuXUVoA2uIB6fEH9HyJ7234s5mBJ5wTx20njxplLZQgLaOfpmPQA7X2t5AX6tIPbog==", + "dev": true, + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "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 + "dev": true, + "engines": { + "node": ">=8" + } }, - "@jest/console": { + "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, - "requires": { + "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" } }, - "@jest/core": { + "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, - "requires": { + "dependencies": { "@jest/console": "^29.7.0", "@jest/reporters": "^29.7.0", "@jest/test-result": "^29.7.0", @@ -660,71 +1288,97 @@ "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 + } } }, - "@jest/environment": { + "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, - "requires": { + "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" } }, - "@jest/expect": { + "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, - "requires": { + "dependencies": { "expect": "^29.7.0", "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "@jest/expect-utils": { + "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, - "requires": { + "dependencies": { "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "@jest/fake-timers": { + "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, - "requires": { + "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" } }, - "@jest/globals": { + "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, - "requires": { + "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" } }, - "@jest/reporters": { + "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, - "requires": { + "dependencies": { "@bcoe/v8-coverage": "^0.2.3", "@jest/console": "^29.7.0", "@jest/test-result": "^29.7.0", @@ -749,58 +1403,81 @@ "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 + } } }, - "@jest/schemas": { + "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, - "requires": { + "dependencies": { "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "@jest/source-map": { + "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, - "requires": { + "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" } }, - "@jest/test-result": { + "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, - "requires": { + "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" } }, - "@jest/test-sequencer": { + "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, - "requires": { + "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" } }, - "@jest/transform": { + "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, - "requires": { + "dependencies": { "@babel/core": "^7.11.6", "@jest/types": "^29.6.3", "@jridgewell/trace-mapping": "^0.3.18", @@ -816,74 +1493,86 @@ "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" } }, - "@jest/types": { + "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, - "requires": { + "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" } }, - "@jridgewell/gen-mapping": { + "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, - "requires": { + "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, - "@jridgewell/remapping": { + "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, - "requires": { + "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, - "@jridgewell/resolve-uri": { + "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 + "dev": true, + "engines": { + "node": ">=6.0.0" + } }, - "@jridgewell/sourcemap-codec": { + "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 }, - "@jridgewell/trace-mapping": { + "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, - "requires": { + "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "@lukeed/csprng": { + "node_modules/@lukeed/csprng": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@lukeed/csprng/-/csprng-1.1.0.tgz", - "integrity": "sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==" + "integrity": "sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==", + "engines": { + "node": ">=8" + } }, - "@mapbox/node-pre-gyp": { + "node_modules/@mapbox/node-pre-gyp": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", - "requires": { + "dependencies": { "detect-libc": "^2.0.0", "https-proxy-agent": "^5.0.0", "make-dir": "^3.1.0", @@ -893,109 +1582,271 @@ "rimraf": "^3.0.2", "semver": "^7.3.5", "tar": "^6.1.11" + }, + "bin": { + "node-pre-gyp": "bin/node-pre-gyp" } }, - "@microsoft/tsdoc": { + "node_modules/@microsoft/tsdoc": { "version": "0.15.1", "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.15.1.tgz", "integrity": "sha512-4aErSrCR/On/e5G2hDP0wjooqDdauzEbIq8hIkIe5pXV0rtWJZvdCEKL0ykZxex+IxIwBp0eGeV48hQN07dXtw==" }, - "@nestjs/common": { + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.4.tgz", + "integrity": "sha512-LCkGo6JDfaBhgST7UpPWgNgLINpcpabaHfyz5OBx75nUYxBsaEPxjnyNjWpeb/xBup/682QnBfRBy2/LvPutZQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.4.tgz", + "integrity": "sha512-zExlW9zUJKZH/tOtVMttwjKa4Xm/3KcNjnE3dPN92uCktwavMxpgCA3MoJK/DOnTWsQgo224OaST27/mPNAf+w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.4.tgz", + "integrity": "sha512-Tg3yX65f5GbtXLkrYEHE5oibZG9epyYWas7FogTTEJeDEF9JlXJzKgXaNhT3UXlTOeA+AfZpYZYZ0uPj7Cfquw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.4.tgz", + "integrity": "sha512-dgX0P/9wGPJeHFBG+ZmhgE6bmtMt7NP5CRBGyyktpopdk/mW4POnrpQsSLtKI1dwpc+pPLuXHDh6vvskyQE/sw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.4.tgz", + "integrity": "sha512-8TNXMEjJc3QEy7R/x1INhgiU+XakDAFUzBhaz7+Rbrs8NH5UQeHQxxmzsSBJGyV6I1jW79undiQm8tOI+D+8FQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.4.tgz", + "integrity": "sha512-CmCXPQrkbwExx3j946/PtHWHbYJiCRBRDl4BlkRQcJB/YOwQxJRTpoo7aTsortjgoJ1x7opzTSxn7C+ASSLVjQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@nestjs/common": { "version": "10.4.22", "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-10.4.22.tgz", "integrity": "sha512-fxJ4v85nDHaqT1PmfNCQ37b/jcv2OojtXTaK1P2uAXhzLf9qq6WNUOFvxBrV4fhQek1EQoT1o9oj5xAZmv3NRw==", - "requires": { + "dependencies": { "file-type": "20.4.1", "iterare": "1.2.1", "tslib": "2.8.1", "uid": "2.0.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "class-transformer": "*", + "class-validator": "*", + "reflect-metadata": "^0.1.12 || ^0.2.0", + "rxjs": "^7.1.0" + }, + "peerDependenciesMeta": { + "class-transformer": { + "optional": true + }, + "class-validator": { + "optional": true + } } }, - "@nestjs/config": { + "node_modules/@nestjs/config": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/@nestjs/config/-/config-3.3.0.tgz", "integrity": "sha512-pdGTp8m9d0ZCrjTpjkUbZx6gyf2IKf+7zlkrPNMsJzYZ4bFRRTpXrnj+556/5uiI6AfL5mMrJc2u7dB6bvM+VA==", - "requires": { + "dependencies": { "dotenv": "16.4.5", "dotenv-expand": "10.0.0", "lodash": "4.17.21" }, - "dependencies": { - "dotenv": { - "version": "16.4.5", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", - "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==" - } + "peerDependencies": { + "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", + "rxjs": "^7.1.0" } }, - "@nestjs/core": { + "node_modules/@nestjs/config/node_modules/dotenv": { + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/@nestjs/core": { "version": "10.4.22", "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-10.4.22.tgz", "integrity": "sha512-6IX9+VwjiKtCjx+mXVPncpkQ5ZjKfmssOZPFexmT+6T9H9wZ3svpYACAo7+9e7Nr9DZSoRZw3pffkJP7Z0UjaA==", - "requires": { + "hasInstallScript": true, + "dependencies": { "@nuxtjs/opencollective": "0.3.2", "fast-safe-stringify": "2.1.1", "iterare": "1.2.1", "path-to-regexp": "3.3.0", "tslib": "2.8.1", "uid": "2.0.2" - } - }, - "@nestjs/jwt": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/@nestjs/jwt/-/jwt-10.2.0.tgz", - "integrity": "sha512-x8cG90SURkEiLOehNaN2aRlotxT0KZESUliOPKKnjWiyJOcWurkF3w345WOX0P4MgFzUjGoZ1Sy0aZnxeihT0g==", - "requires": { - "@types/jsonwebtoken": "9.0.5", - "jsonwebtoken": "9.0.2" }, - "dependencies": { - "jsonwebtoken": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", - "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", - "requires": { - "jws": "^3.2.2", - "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" - } + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "@nestjs/common": "^10.0.0", + "@nestjs/microservices": "^10.0.0", + "@nestjs/platform-express": "^10.0.0", + "@nestjs/websockets": "^10.0.0", + "reflect-metadata": "^0.1.12 || ^0.2.0", + "rxjs": "^7.1.0" + }, + "peerDependenciesMeta": { + "@nestjs/microservices": { + "optional": true + }, + "@nestjs/platform-express": { + "optional": true + }, + "@nestjs/websockets": { + "optional": true } } }, - "@nestjs/mapped-types": { + "node_modules/@nestjs/jwt": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/@nestjs/jwt/-/jwt-10.2.0.tgz", + "integrity": "sha512-x8cG90SURkEiLOehNaN2aRlotxT0KZESUliOPKKnjWiyJOcWurkF3w345WOX0P4MgFzUjGoZ1Sy0aZnxeihT0g==", + "dependencies": { + "@types/jsonwebtoken": "9.0.5", + "jsonwebtoken": "9.0.2" + }, + "peerDependencies": { + "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0" + } + }, + "node_modules/@nestjs/jwt/node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "dependencies": { + "jws": "^3.2.2", + "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/@nestjs/mapped-types": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-2.1.1.tgz", - "integrity": "sha512-SCCoMEJ6jdeI5h/N+KCVF1+pmg/hmEkNA5nHTS8Gvww7T/LCl4o1gFLinw2iQ60w7slFkszHcGLKGdazVI4F8A==" + "integrity": "sha512-SCCoMEJ6jdeI5h/N+KCVF1+pmg/hmEkNA5nHTS8Gvww7T/LCl4o1gFLinw2iQ60w7slFkszHcGLKGdazVI4F8A==", + "peerDependencies": { + "@nestjs/common": "^10.0.0 || ^11.0.0", + "class-transformer": "^0.4.0 || ^0.5.0", + "class-validator": "^0.13.0 || ^0.14.0 || ^0.15.0", + "reflect-metadata": "^0.1.12 || ^0.2.0" + }, + "peerDependenciesMeta": { + "class-transformer": { + "optional": true + }, + "class-validator": { + "optional": true + } + } }, - "@nestjs/passport": { + "node_modules/@nestjs/passport": { "version": "10.0.3", "resolved": "https://registry.npmjs.org/@nestjs/passport/-/passport-10.0.3.tgz", - "integrity": "sha512-znJ9Y4S8ZDVY+j4doWAJ8EuuVO7SkQN3yOBmzxbGaXbvcSwFDAdGJ+OMCg52NdzIO4tQoN4pYKx8W6M0ArfFRQ==" + "integrity": "sha512-znJ9Y4S8ZDVY+j4doWAJ8EuuVO7SkQN3yOBmzxbGaXbvcSwFDAdGJ+OMCg52NdzIO4tQoN4pYKx8W6M0ArfFRQ==", + "peerDependencies": { + "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", + "passport": "^0.4.0 || ^0.5.0 || ^0.6.0 || ^0.7.0" + } }, - "@nestjs/platform-express": { + "node_modules/@nestjs/platform-express": { "version": "10.4.22", "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.4.22.tgz", "integrity": "sha512-ySSq7Py/DFozzZdNDH67m/vHoeVdphDniWBnl6q5QVoXldDdrZIHLXLRMPayTDh5A95nt7jjJzmD4qpTbNQ6tA==", - "requires": { + "dependencies": { "body-parser": "1.20.4", "cors": "2.8.5", "express": "4.22.1", "multer": "2.0.2", "tslib": "2.8.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "@nestjs/common": "^10.0.0", + "@nestjs/core": "^10.0.0" } }, - "@nestjs/swagger": { + "node_modules/@nestjs/swagger": { "version": "7.4.2", "resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-7.4.2.tgz", "integrity": "sha512-Mu6TEn1M/owIvAx2B4DUQObQXqo2028R2s9rSZ/hJEgBK95+doTwS0DjmVA2wTeZTyVtXOoN7CsoM5pONBzvKQ==", - "requires": { + "dependencies": { "@microsoft/tsdoc": "^0.15.0", "@nestjs/mapped-types": "2.0.5", "js-yaml": "4.1.0", @@ -1003,162 +1854,351 @@ "path-to-regexp": "3.3.0", "swagger-ui-dist": "5.17.14" }, - "dependencies": { - "@nestjs/mapped-types": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-2.0.5.tgz", - "integrity": "sha512-bSJv4pd6EY99NX9CjBIyn4TVDoSit82DUZlL4I3bqNfy5Gt+gXTa86i3I/i0iIV9P4hntcGM5GyO+FhZAhxtyg==" + "peerDependencies": { + "@fastify/static": "^6.0.0 || ^7.0.0", + "@nestjs/common": "^9.0.0 || ^10.0.0", + "@nestjs/core": "^9.0.0 || ^10.0.0", + "class-transformer": "*", + "class-validator": "*", + "reflect-metadata": "^0.1.12 || ^0.2.0" + }, + "peerDependenciesMeta": { + "@fastify/static": { + "optional": true + }, + "class-transformer": { + "optional": true + }, + "class-validator": { + "optional": true } } }, - "@noble/hashes": { + "node_modules/@nestjs/swagger/node_modules/@nestjs/mapped-types": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-2.0.5.tgz", + "integrity": "sha512-bSJv4pd6EY99NX9CjBIyn4TVDoSit82DUZlL4I3bqNfy5Gt+gXTa86i3I/i0iIV9P4hntcGM5GyO+FhZAhxtyg==", + "peerDependencies": { + "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", + "class-transformer": "^0.4.0 || ^0.5.0", + "class-validator": "^0.13.0 || ^0.14.0", + "reflect-metadata": "^0.1.12 || ^0.2.0" + }, + "peerDependenciesMeta": { + "class-transformer": { + "optional": true + }, + "class-validator": { + "optional": true + } + } + }, + "node_modules/@nestjs/testing": { + "version": "10.4.22", + "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-10.4.22.tgz", + "integrity": "sha512-HO9aPus3bAedAC+jKVAA8jTdaj4fs5M9fing4giHrcYV2txe9CvC1l1WAjwQ9RDhEHdugjY4y+FZA/U/YqPZrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "@nestjs/common": "^10.0.0", + "@nestjs/core": "^10.0.0", + "@nestjs/microservices": "^10.0.0", + "@nestjs/platform-express": "^10.0.0" + }, + "peerDependenciesMeta": { + "@nestjs/microservices": { + "optional": true + }, + "@nestjs/platform-express": { + "optional": true + } + } + }, + "node_modules/@noble/hashes": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", - "dev": true + "dev": true, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } }, - "@nuxtjs/opencollective": { + "node_modules/@nuxtjs/opencollective": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/@nuxtjs/opencollective/-/opencollective-0.3.2.tgz", "integrity": "sha512-um0xL3fO7Mf4fDxcqx9KryrB7zgRM5JSlvGN5AGkP6JLM5XEKyjeAiPbNxdXVXQ16isuAhYpvP88NgL2BGd6aA==", - "requires": { + "dependencies": { "chalk": "^4.1.0", "consola": "^2.15.0", "node-fetch": "^2.6.1" + }, + "bin": { + "opencollective": "bin/opencollective.js" + }, + "engines": { + "node": ">=8.0.0", + "npm": ">=5.0.0" } }, - "@paralleldrive/cuid2": { + "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, - "requires": { + "dependencies": { "@noble/hashes": "^1.1.5" } }, - "@pinojs/redact": { + "node_modules/@pinojs/redact": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz", "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==" }, - "@prisma/client": { + "node_modules/@playwright/test": { + "version": "1.62.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.62.1.tgz", + "integrity": "sha512-DTcUc8qii+cpHvtOwggMtBRMjKZHXYWdw8syRYu2vtzuq4Wxphqq4NfCs5Zt44L6mA8rfDfj+PHnxFc/FeK6mQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.62.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@prisma/client": { "version": "5.22.0", "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.22.0.tgz", - "integrity": "sha512-M0SVXfyHnQREBKxCgyo7sffrKttwE6R8PMq330MIUF0pTwjUhLbW84pFDlf06B27XyCR++VtjugEnIHdr07SVA==" + "integrity": "sha512-M0SVXfyHnQREBKxCgyo7sffrKttwE6R8PMq330MIUF0pTwjUhLbW84pFDlf06B27XyCR++VtjugEnIHdr07SVA==", + "hasInstallScript": true, + "engines": { + "node": ">=16.13" + }, + "peerDependencies": { + "prisma": "*" + }, + "peerDependenciesMeta": { + "prisma": { + "optional": true + } + } }, - "@prisma/debug": { + "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==", - "dev": true + "devOptional": true }, - "@prisma/engines": { + "node_modules/@prisma/engines": { "version": "5.22.0", "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.22.0.tgz", "integrity": "sha512-UNjfslWhAt06kVL3CjkuYpHAWSO6L4kDCVPegV6itt7nD1kSJavd3vhgAEhjglLJJKEdJ7oIqDJ+yHk6qO8gPA==", - "dev": true, - "requires": { + "devOptional": true, + "hasInstallScript": true, + "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" } }, - "@prisma/engines-version": { + "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==", - "dev": true + "devOptional": true }, - "@prisma/fetch-engine": { + "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==", - "dev": true, - "requires": { + "devOptional": true, + "dependencies": { "@prisma/debug": "5.22.0", "@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", "@prisma/get-platform": "5.22.0" } }, - "@prisma/get-platform": { + "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==", - "dev": true, - "requires": { + "devOptional": true, + "dependencies": { "@prisma/debug": "5.22.0" } }, - "@sinclair/typebox": { + "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 }, - "@sinonjs/commons": { + "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, - "requires": { + "dependencies": { "type-detect": "4.0.8" } }, - "@sinonjs/fake-timers": { + "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, - "requires": { + "dependencies": { "@sinonjs/commons": "^3.0.0" } }, - "@tokenizer/inflate": { + "node_modules/@smithy/core": { + "version": "3.31.1", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.31.1.tgz", + "integrity": "sha512-CyogUINxvi7C7LDsh8Syo6hVJOT9ckz4rG8dRZfTJ8r91HkMY59PnNooaj7WcHyxEkxPfBAmbgztZU+xTo76lg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.16.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/credential-provider-imds": { + "version": "4.4.16", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.4.16.tgz", + "integrity": "sha512-QfuLWAkLzptffFW980AFeHZFdqds2B64rpEd3uJ6lgs3xVn9QegGMUgUcj+4d7dRrAsya3r58ZKpku97WcFb4w==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.31.1", + "@smithy/types": "^4.16.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/fetch-http-handler": { + "version": "5.6.13", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.6.13.tgz", + "integrity": "sha512-4fW86pEUOMbrD5nkbyl/tTvPHHWJFbuB2odl6ps9lWfHoXf9HWh3Q/Smh59qH1g7+c/BSZghX6bbUk4gsiMs8A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.31.1", + "@smithy/types": "^4.16.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/node-http-handler": { + "version": "4.9.13", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.9.13.tgz", + "integrity": "sha512-Nmd/Nl35zfYrd+a6OO2cDJb3GPh9bgTjIUhcM+JFfjpp8/osCgboDV5nCT1I01Pv6R13eSKDKLSoVa5ZB6Zsfw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.31.1", + "@smithy/types": "^4.16.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/signature-v4": { + "version": "5.6.12", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.6.12.tgz", + "integrity": "sha512-I6KLtq3H0qqSuV9vLglfi8puHqzygzWHOnI4z/Rdoo+q50vvo18vBRdPAvvEtcaKROz7Zn6qnPa14kRfPH6PcQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.31.1", + "@smithy/types": "^4.16.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/types": { + "version": "4.16.1", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.16.1.tgz", + "integrity": "sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@tokenizer/inflate": { "version": "0.2.7", "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.2.7.tgz", "integrity": "sha512-MADQgmZT1eKjp06jpI2yozxaU9uVs4GzzgSL+uEq7bVcJ9V1ZXQkeGNql1fsSI0gMy1vhvNTNbUqrx+pZfJVmg==", - "requires": { + "dependencies": { "debug": "^4.4.0", "fflate": "^0.8.2", "token-types": "^6.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" } }, - "@tokenizer/token": { + "node_modules/@tokenizer/token": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==" }, - "@tsconfig/node10": { + "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 }, - "@tsconfig/node12": { + "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 }, - "@tsconfig/node14": { + "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 }, - "@tsconfig/node16": { + "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 }, - "@types/babel__core": { + "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, - "requires": { + "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", @@ -1166,329 +2206,336 @@ "@types/babel__traverse": "*" } }, - "@types/babel__generator": { + "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, - "requires": { + "dependencies": { "@babel/types": "^7.0.0" } }, - "@types/babel__template": { + "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, - "requires": { + "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" } }, - "@types/babel__traverse": { + "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, - "requires": { + "dependencies": { "@babel/types": "^7.28.2" } }, - "@types/bcrypt": { + "node_modules/@types/bcrypt": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-5.0.2.tgz", "integrity": "sha512-6atioO8Y75fNcbmj0G7UjI9lXN2pQ/IGJ2FWT4a/btd0Lk9lQalHLKhkgKVZ3r+spnmWUKfbMi1GEe9wyHQfNQ==", "dev": true, - "requires": { + "dependencies": { "@types/node": "*" } }, - "@types/body-parser": { + "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, - "requires": { + "dependencies": { "@types/connect": "*", "@types/node": "*" } }, - "@types/connect": { + "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, - "requires": { + "dependencies": { "@types/node": "*" } }, - "@types/cookiejar": { + "node_modules/@types/cookie-parser": { + "version": "1.4.10", + "resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.10.tgz", + "integrity": "sha512-B4xqkqfZ8Wek+rCOeRxsjMS9OgvzebEzzLYw7NHYuvzb7IdxOkI0ZHGgeEBX4PUM7QGVvNSK60T3OvWj3YfBRg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/express": "*" + } + }, + "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 }, - "@types/estree": { + "node_modules/@types/estree": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", "dev": true }, - "@types/express": { + "node_modules/@types/express": { "version": "5.0.6", "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz", "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", "dev": true, - "requires": { + "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", "@types/serve-static": "^2" } }, - "@types/express-serve-static-core": { + "node_modules/@types/express-serve-static-core": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.2.tgz", "integrity": "sha512-d3KvEXBSo/lOAMc2u6fkyDHBvetBHeqD7wm/AcXfLpSOQwlmG9D/aQ0SFswVjv05p7ullQS7Mjohj6/VdbZuTg==", "dev": true, - "requires": { + "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, - "@types/graceful-fs": { + "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, - "requires": { + "dependencies": { "@types/node": "*" } }, - "@types/http-errors": { + "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 }, - "@types/istanbul-lib-coverage": { + "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 }, - "@types/istanbul-lib-report": { + "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, - "requires": { + "dependencies": { "@types/istanbul-lib-coverage": "*" } }, - "@types/istanbul-reports": { + "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, - "requires": { + "dependencies": { "@types/istanbul-lib-report": "*" } }, - "@types/jest": { + "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, - "requires": { + "dependencies": { "expect": "^29.0.0", "pretty-format": "^29.0.0" } }, - "@types/json-schema": { + "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true }, - "@types/jsonwebtoken": { + "node_modules/@types/jsonwebtoken": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.5.tgz", "integrity": "sha512-VRLSGzik+Unrup6BsouBeHsf4d1hOEgYWTm/7Nmw1sXoN1+tRly/Gy/po3yeahnP4jfnQWWAhQAqcNfH7ngOkA==", - "requires": { - "@types/node": "*" - }, "dependencies": { - "@types/node": { - "version": "26.1.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.2.tgz", - "integrity": "sha512-Vu4a5UFA9rIIFJ7rB/Vaafh9lrCQszopTCx6KjFboXTGQbPNasehVR5TEiithSDGyd1DEiUByggTZsg8jukeIg==", - "requires": { - "undici-types": "~8.3.0" - } - } + "@types/node": "*" } }, - "@types/methods": { + "node_modules/@types/jsonwebtoken/node_modules/@types/node": { + "version": "26.1.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.2.tgz", + "integrity": "sha512-Vu4a5UFA9rIIFJ7rB/Vaafh9lrCQszopTCx6KjFboXTGQbPNasehVR5TEiithSDGyd1DEiUByggTZsg8jukeIg==", + "dependencies": { + "undici-types": "~8.3.0" + } + }, + "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 }, - "@types/node": { + "node_modules/@types/node": { "version": "22.20.1", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.1.tgz", "integrity": "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==", "dev": true, - "requires": { - "undici-types": "~6.21.0" - }, + "license": "MIT", "dependencies": { - "undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true - } + "undici-types": "~6.21.0" } }, - "@types/passport": { + "node_modules/@types/node/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==", + "dev": true + }, + "node_modules/@types/passport": { "version": "1.0.17", "resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.17.tgz", "integrity": "sha512-aciLyx+wDwT2t2/kJGJR2AEeBz0nJU4WuRX04Wu9Dqc5lSUtwu0WERPHYsLhF9PtseiAMPBGNUOtFjxZ56prsg==", "dev": true, - "requires": { + "dependencies": { "@types/express": "*" } }, - "@types/passport-jwt": { + "node_modules/@types/passport-jwt": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@types/passport-jwt/-/passport-jwt-4.0.1.tgz", "integrity": "sha512-Y0Ykz6nWP4jpxgEUYq8NoVZeCQPo1ZndJLfapI249g1jHChvRfZRO/LS3tqu26YgAS/laI1qx98sYGz0IalRXQ==", "dev": true, - "requires": { + "dependencies": { "@types/jsonwebtoken": "*", "@types/passport-strategy": "*" } }, - "@types/passport-strategy": { + "node_modules/@types/passport-strategy": { "version": "0.2.38", "resolved": "https://registry.npmjs.org/@types/passport-strategy/-/passport-strategy-0.2.38.tgz", "integrity": "sha512-GC6eMqqojOooq993Tmnmp7AUTbbQSgilyvpCYQjT+H6JfG/g6RGc7nXEniZlp0zyKJ0WUdOiZWLBZft9Yug1uA==", "dev": true, - "requires": { + "dependencies": { "@types/express": "*", "@types/passport": "*" } }, - "@types/qs": { + "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 }, - "@types/range-parser": { + "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 }, - "@types/send": { + "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, - "requires": { + "dependencies": { "@types/node": "*" } }, - "@types/serve-static": { + "node_modules/@types/serve-static": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz", "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==", "dev": true, - "requires": { + "dependencies": { "@types/http-errors": "*", "@types/node": "*" } }, - "@types/stack-utils": { + "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 }, - "@types/strip-bom": { + "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 }, - "@types/strip-json-comments": { + "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 }, - "@types/superagent": { + "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, - "requires": { + "dependencies": { "@types/cookiejar": "^2.1.5", "@types/methods": "^1.1.4", "@types/node": "*", "form-data": "^4.0.0" } }, - "@types/supertest": { + "node_modules/@types/supertest": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/@types/supertest/-/supertest-6.0.3.tgz", "integrity": "sha512-8WzXq62EXFhJ7QsH3Ocb/iKQ/Ty9ZVWnVzoTKc9tyyFRRF3a74Tk2+TLFgaFFw364Ere+npzHKEJ6ga2LzIL7w==", "dev": true, - "requires": { + "dependencies": { "@types/methods": "^1.1.4", "@types/superagent": "^8.1.0" } }, - "@types/swagger-ui-express": { + "node_modules/@types/swagger-ui-express": { "version": "4.1.8", "resolved": "https://registry.npmjs.org/@types/swagger-ui-express/-/swagger-ui-express-4.1.8.tgz", "integrity": "sha512-AhZV8/EIreHFmBV5wAs0gzJUNq9JbbSXgJLQubCC0jtIo6prnI9MIRRxnU4MZX9RB9yXxF1V4R7jtLl/Wcj31g==", "dev": true, - "requires": { + "dependencies": { "@types/express": "*", "@types/serve-static": "*" } }, - "@types/validator": { + "node_modules/@types/validator": { "version": "13.15.10", "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.10.tgz", "integrity": "sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==" }, - "@types/yargs": { + "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, - "requires": { + "dependencies": { "@types/yargs-parser": "*" } }, - "@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 }, - "@typescript-eslint/eslint-plugin": { + "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.65.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.65.0.tgz", "integrity": "sha512-IEgob78X12rHpUmtcwFsXhZdVGJtwTVP8FiCLZkR6GlYVrl2PcuB+KhCE5BlVC/eQpQnu8WXRtkHZuPar+gCRA==", "dev": true, - "requires": { + "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.65.0", "@typescript-eslint/type-utils": "8.65.0", @@ -1497,73 +2544,141 @@ "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.65.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, - "@typescript-eslint/parser": { + "node_modules/@typescript-eslint/parser": { "version": "8.65.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.65.0.tgz", "integrity": "sha512-CZ4nMxWwgu1HEEFNkeaCptra9QCtkmKdgf3sWh1rl1trIhmxLilgTV4cwcbQ4wemnT4sWQN8CaKOmdYx+g2gMA==", "dev": true, - "requires": { + "dependencies": { "@typescript-eslint/scope-manager": "8.65.0", "@typescript-eslint/types": "8.65.0", "@typescript-eslint/typescript-estree": "8.65.0", "@typescript-eslint/visitor-keys": "8.65.0", "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, - "@typescript-eslint/project-service": { + "node_modules/@typescript-eslint/project-service": { "version": "8.65.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.65.0.tgz", "integrity": "sha512-SxnPhbTsGahizDgbu7oqFH/xVtzIqMd/s+WtnSxNxJZJpLbdT5IPdzg8EZxO3+PoKahXmwJLeNQOpKJb3/bi7Q==", "dev": true, - "requires": { + "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.65.0", "@typescript-eslint/types": "^8.65.0", "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" } }, - "@typescript-eslint/scope-manager": { + "node_modules/@typescript-eslint/scope-manager": { "version": "8.65.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.65.0.tgz", "integrity": "sha512-Esbl8OSYiVxBokYgWPf7VVWg/BE798wXhimnn9ML9Pt5qoDf8bfQlgjlKXR/k98+AcNzlLKYrpCcrcuZ9DZLgg==", "dev": true, - "requires": { + "dependencies": { "@typescript-eslint/types": "8.65.0", "@typescript-eslint/visitor-keys": "8.65.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "@typescript-eslint/tsconfig-utils": { + "node_modules/@typescript-eslint/tsconfig-utils": { "version": "8.65.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.65.0.tgz", "integrity": "sha512-j6GzGqCiRdA7Qhur2VVmKZAkBLfnHFQfx4TaJGL9RMveZqCo48jSHHO0DTgizEnGhtWnqmbtCUSrqSkdiY/0Hg==", - "dev": true + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } }, - "@typescript-eslint/type-utils": { + "node_modules/@typescript-eslint/type-utils": { "version": "8.65.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.65.0.tgz", "integrity": "sha512-YjaZ7PRI5qY7ax2L3PbvX0rRyGtipAReCWs0mhhDBHjH/vl0g0BonaGXrKdKpMbIIsMIwDgbk/xzkBTyAltS5g==", "dev": true, - "requires": { + "dependencies": { "@typescript-eslint/types": "8.65.0", "@typescript-eslint/typescript-estree": "8.65.0", "@typescript-eslint/utils": "8.65.0", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, - "@typescript-eslint/types": { + "node_modules/@typescript-eslint/types": { "version": "8.65.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.65.0.tgz", "integrity": "sha512-JSSwWNy+H0E/01jJEM+hrX6N0OFDzFzeIhHFSAS01tlVaevpG8cFyYRPhS5yjGOvBUx3sqQHVMjCL1CAZZMxBg==", - "dev": true + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } }, - "@typescript-eslint/typescript-estree": { + "node_modules/@typescript-eslint/typescript-estree": { "version": "8.65.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.65.0.tgz", "integrity": "sha512-JboAE2swaYt4tb1fHhHTABE2K+OLy09XfcTbhnk4Pw96f9dd2e9iYsJ28gBggHlo5z5x1rkyWvcPoTuNTd4oGg==", "dev": true, - "requires": { + "dependencies": { "@typescript-eslint/project-service": "8.65.0", "@typescript-eslint/tsconfig-utils": "8.65.0", "@typescript-eslint/types": "8.65.0", @@ -1574,200 +2689,309 @@ "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, - "dependencies": { - "balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", - "dev": true - }, - "brace-expansion": { - "version": "5.0.9", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", - "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", - "dev": true, - "requires": { - "balanced-match": "^4.0.2" - } - }, - "minimatch": { - "version": "10.2.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", - "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", - "dev": true, - "requires": { - "brace-expansion": "^5.0.8" - } - } + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" } }, - "@typescript-eslint/utils": { + "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "dev": true, + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { "version": "8.65.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.65.0.tgz", "integrity": "sha512-gXiwIHsYreboxeJucHKPvgwl7dXt50mF8s1/c00cP/WoVTyWKFdtfhRWwZiXYFU5H2O8vVoSLNrexFZjYS/SGA==", "dev": true, - "requires": { + "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.65.0", "@typescript-eslint/types": "8.65.0", "@typescript-eslint/typescript-estree": "8.65.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, - "@typescript-eslint/visitor-keys": { + "node_modules/@typescript-eslint/visitor-keys": { "version": "8.65.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.65.0.tgz", "integrity": "sha512-8C71BQkGjiMmXtop7pHVJu1l2NNShFdkCyD6a2ezzs5vU/L3LRtb69EtcteFwz0mYMPzIgOw0n6OV4VBUWZd7A==", "dev": true, - "requires": { + "dependencies": { "@typescript-eslint/types": "8.65.0", "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "abbrev": { + "node_modules/abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" }, - "accepts": { + "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==", - "requires": { + "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" } }, - "acorn": { + "node_modules/acorn": { "version": "8.18.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", - "dev": true + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } }, - "acorn-jsx": { + "node_modules/acorn-jsx": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } }, - "acorn-walk": { + "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, - "requires": { + "dependencies": { "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" } }, - "agent-base": { + "node_modules/agent-base": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "requires": { + "dependencies": { "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" } }, - "ajv": { + "node_modules/ajv": { "version": "6.15.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", "dev": true, - "requires": { + "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" } }, - "ansi-escapes": { + "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, - "requires": { + "dependencies": { "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "ansi-regex": { + "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==" + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } }, - "ansi-styles": { + "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==", - "requires": { + "dependencies": { "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "anymatch": { + "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, - "requires": { + "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" } }, - "append-field": { + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/append-field": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==" }, - "aproba": { + "node_modules/aproba": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.1.0.tgz", "integrity": "sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==" }, - "are-we-there-yet": { + "node_modules/are-we-there-yet": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", - "requires": { + "deprecated": "This package is no longer supported.", + "dependencies": { "delegates": "^1.0.0", "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=10" } }, - "arg": { + "node_modules/arg": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", "dev": true }, - "argparse": { + "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" }, - "array-flatten": { + "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==" }, - "asap": { + "node_modules/asap": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", "dev": true }, - "asynckit": { + "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 + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, - "atomic-sleep": { + "node_modules/atomic-sleep": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", - "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==" + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "engines": { + "node": ">=8.0.0" + } }, - "babel-jest": { + "node_modules/axios": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.19.0.tgz", + "integrity": "sha512-ht/iuYZXEjFxLH/Hkezgd7m6JKlHHXEUSneaDz8uZe1Gj5QZtCnpyDsckvAiEnT89OEbCLmnte4R4sn7P0EKFw==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.16.0", + "form-data": "^4.0.6", + "https-proxy-agent": "^5.0.1", + "proxy-from-env": "^2.1.0" + } + }, + "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, - "requires": { + "dependencies": { "@jest/transform": "^29.7.0", "@types/babel__core": "^7.1.14", "babel-plugin-istanbul": "^6.1.1", @@ -1775,60 +2999,76 @@ "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" } }, - "babel-plugin-istanbul": { + "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, - "requires": { + "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" }, - "dependencies": { - "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, - "requires": { - "@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" - } - }, - "semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true - } + "engines": { + "node": ">=8" } }, - "babel-plugin-jest-hoist": { + "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, + "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-istanbul/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, + "bin": { + "semver": "bin/semver.js" + } + }, + "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, - "requires": { + "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" } }, - "babel-preset-current-node-syntax": { + "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, - "requires": { + "dependencies": { "@babel/plugin-syntax-async-generators": "^7.8.4", "@babel/plugin-syntax-bigint": "^7.8.3", "@babel/plugin-syntax-class-properties": "^7.12.13", @@ -1844,49 +3084,74 @@ "@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" } }, - "babel-preset-jest": { + "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, - "requires": { + "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" } }, - "balanced-match": { + "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==" }, - "baseline-browser-mapping": { + "node_modules/baseline-browser-mapping": { "version": "2.11.7", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.7.tgz", "integrity": "sha512-APw5YuIQAg6L9w4sHDI6j26DGFJI6RpYOhnkMPdC9lWbkKvsyPHzDsve1yd73lk21yz7Y09Kci8B2Pp9FonzWA==", - "dev": true + "dev": true, + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } }, - "bcrypt": { + "node_modules/bcrypt": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz", "integrity": "sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww==", - "requires": { + "hasInstallScript": true, + "dependencies": { "@mapbox/node-pre-gyp": "^1.0.11", "node-addon-api": "^5.0.0" + }, + "engines": { + "node": ">= 10.0.0" } }, - "binary-extensions": { + "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 + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, - "body-parser": { + "node_modules/body-parser": { "version": "1.20.4", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", - "requires": { + "dependencies": { "bytes": "~3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", @@ -1900,336 +3165,568 @@ "type-is": "~1.6.18", "unpipe": "~1.0.0" }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "requires": { - "ms": "2.0.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - } + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" } }, - "brace-expansion": { + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/bowser": { + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.14.1.tgz", + "integrity": "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==", + "license": "MIT" + }, + "node_modules/brace-expansion": { "version": "1.1.18", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", - "requires": { + "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, - "braces": { + "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, - "requires": { + "dependencies": { "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" } }, - "browserslist": { + "node_modules/browserslist": { "version": "4.28.7", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.7.tgz", "integrity": "sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw==", "dev": true, - "requires": { + "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" + } + ], + "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" } }, - "bs-logger": { + "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, - "requires": { + "dependencies": { "fast-json-stable-stringify": "2.x" + }, + "engines": { + "node": ">= 6" } }, - "bser": { + "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, - "requires": { + "dependencies": { "node-int64": "^0.4.0" } }, - "buffer-equal-constant-time": { + "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==" }, - "buffer-from": { + "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==" }, - "busboy": { + "node_modules/bullmq": { + "version": "1.91.1", + "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-1.91.1.tgz", + "integrity": "sha512-u7dat9I8ZwouZ651AMZkBSvB6NVUPpnAjd4iokd9DM41whqIBnDjuL11h7+kEjcpiDKj6E+wxZiER00FqirZQg==", + "license": "MIT", + "dependencies": { + "cron-parser": "^4.6.0", + "get-port": "6.1.2", + "glob": "^8.0.3", + "ioredis": "^5.2.2", + "lodash": "^4.17.21", + "msgpackr": "^1.6.2", + "semver": "^7.3.7", + "tslib": "^2.0.0", + "uuid": "^9.0.0" + } + }, + "node_modules/bullmq/node_modules/brace-expansion": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", + "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/bullmq/node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/bullmq/node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/bullmq/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/busboy": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", - "requires": { + "dependencies": { "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" } }, - "bytes": { + "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==" + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } }, - "call-bind-apply-helpers": { + "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==", - "requires": { + "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" } }, - "call-bound": { + "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==", - "requires": { + "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "callsites": { + "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 + "dev": true, + "engines": { + "node": ">=6" + } }, - "camelcase": { + "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 + "dev": true, + "engines": { + "node": ">=6" + } }, - "caniuse-lite": { + "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 + "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" + } + ] }, - "chalk": { + "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "requires": { + "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "char-regex": { + "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 + "dev": true, + "engines": { + "node": ">=10" + } }, - "chokidar": { + "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, - "requires": { + "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", - "fsevents": "~2.3.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" }, - "dependencies": { - "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, - "requires": { - "is-glob": "^4.0.1" - } - } + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" } }, - "chownr": { + "node_modules/chokidar/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, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/chownr": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", - "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==" + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "engines": { + "node": ">=10" + } }, - "ci-info": { + "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 + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "engines": { + "node": ">=8" + } }, - "cjs-module-lexer": { + "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 }, - "class-transformer": { + "node_modules/class-transformer": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz", "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==" }, - "class-validator": { + "node_modules/class-validator": { "version": "0.14.4", "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.4.tgz", "integrity": "sha512-AwNusCCam51q703dW82x95tOqQp6oC9HNUl724KxJJOfnKscI8dOloXFgyez7LbTTKWuRBA37FScqVbJEoq8Yw==", - "requires": { + "dependencies": { "@types/validator": "^13.15.3", "libphonenumber-js": "^1.11.1", "validator": "^13.15.22" } }, - "cliui": { + "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, - "requires": { + "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" } }, - "co": { + "node_modules/cluster-key-slot": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.1.tgz", + "integrity": "sha512-rwHwUfXL40Chm1r08yrhU3qpUvdVlgkKNeyeGPOxnW8/SyVDvgRaed/Uz54AqWNaTCAThlj6QAs3TZcKI0xDEw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10.0" + } + }, + "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 + "dev": true, + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } }, - "collect-v8-coverage": { + "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 }, - "color-convert": { + "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==", - "requires": { + "dependencies": { "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" } }, - "color-name": { + "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==" }, - "color-support": { + "node_modules/color-support": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", - "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==" + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "bin": { + "color-support": "bin.js" + } }, - "combined-stream": { + "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, - "requires": { + "dependencies": { "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" } }, - "component-emitter": { + "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 + "dev": true, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, - "concat-map": { + "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, - "concat-stream": { + "node_modules/concat-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", - "requires": { + "engines": [ + "node >= 6.0" + ], + "dependencies": { "buffer-from": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.0.2", "typedarray": "^0.0.6" } }, - "consola": { + "node_modules/consola": { "version": "2.15.3", "resolved": "https://registry.npmjs.org/consola/-/consola-2.15.3.tgz", "integrity": "sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw==" }, - "console-control-strings": { + "node_modules/console-control-strings": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==" }, - "content-disposition": { + "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==", - "requires": { + "dependencies": { "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" } }, - "content-type": { + "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==" + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "engines": { + "node": ">= 0.6" + } }, - "convert-source-map": { + "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 }, - "cookie": { + "node_modules/cookie": { "version": "0.7.2", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==" + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "engines": { + "node": ">= 0.6" + } }, - "cookie-signature": { + "node_modules/cookie-parser": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz", + "integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==", + "license": "MIT", + "dependencies": { + "cookie": "0.7.2", + "cookie-signature": "1.0.6" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/cookie-parser/node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, + "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==" }, - "cookiejar": { + "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 }, - "cors": { + "node_modules/cors": { "version": "2.8.5", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", - "requires": { + "dependencies": { "object-assign": "^4", "vary": "^1" + }, + "engines": { + "node": ">= 0.10" } }, - "create-jest": { + "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, - "requires": { + "dependencies": { "@jest/types": "^29.6.3", "chalk": "^4.0.0", "exit": "^0.1.2", @@ -2237,231 +3734,340 @@ "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" } }, - "create-require": { + "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 }, - "cross-spawn": { + "node_modules/cron-parser": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-4.9.0.tgz", + "integrity": "sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==", + "deprecated": "v4 is no longer maintained, upgrade to v5", + "license": "MIT", + "dependencies": { + "luxon": "^3.2.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "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, - "requires": { + "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" } }, - "debug": { + "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "requires": { + "dependencies": { "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, - "dedent": { + "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 + "dev": true, + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } }, - "deep-is": { + "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true }, - "deepmerge": { + "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 + "dev": true, + "engines": { + "node": ">=0.10.0" + } }, - "delayed-stream": { + "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 + "engines": { + "node": ">=0.4.0" + } }, - "delegates": { + "node_modules/delegates": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==" }, - "depd": { + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, + "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==" + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } }, - "destroy": { + "node_modules/destroy": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==" + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } }, - "detect-libc": { + "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==" + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "engines": { + "node": ">=8" + } }, - "detect-newline": { + "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 + "dev": true, + "engines": { + "node": ">=8" + } }, - "dezalgo": { + "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, - "requires": { + "dependencies": { "asap": "^2.0.0", "wrappy": "1" } }, - "diff": { + "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 + "dev": true, + "engines": { + "node": ">=0.3.1" + } }, - "diff-sequences": { + "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 + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } }, - "dotenv": { + "node_modules/dotenv": { "version": "16.6.1", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", - "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==" + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } }, - "dotenv-expand": { + "node_modules/dotenv-expand": { "version": "10.0.0", "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-10.0.0.tgz", - "integrity": "sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A==" + "integrity": "sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A==", + "engines": { + "node": ">=12" + } }, - "dunder-proto": { + "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==", - "requires": { + "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" } }, - "dynamic-dedupe": { + "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, - "requires": { + "dependencies": { "xtend": "^4.0.0" } }, - "ecdsa-sig-formatter": { + "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==", - "requires": { + "dependencies": { "safe-buffer": "^5.0.1" } }, - "ee-first": { + "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==" }, - "electron-to-chromium": { + "node_modules/electron-to-chromium": { "version": "1.5.398", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.398.tgz", "integrity": "sha512-AsvhAxopJGh6museTDMIjn6JpDYOfgu4RLlygomt87MUwBUqTfd/1EiPtx10/LZE8xpTvkP2E9Gafq7lkLtodQ==", "dev": true }, - "emittery": { + "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 + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } }, - "emoji-regex": { + "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==" }, - "encodeurl": { + "node_modules/encodeurl": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==" + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "engines": { + "node": ">= 0.8" + } }, - "error-ex": { + "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, - "requires": { + "dependencies": { "is-arrayish": "^0.2.1" } }, - "es-define-property": { + "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==" + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "engines": { + "node": ">= 0.4" + } }, - "es-errors": { + "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==" + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } }, - "es-object-atoms": { + "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==", - "requires": { + "dependencies": { "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" } }, - "es-set-tostringtag": { + "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, - "requires": { + "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" } }, - "escalade": { + "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 + "dev": true, + "engines": { + "node": ">=6" + } }, - "escape-html": { + "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==" }, - "escape-string-regexp": { + "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 + "dev": true, + "engines": { + "node": ">=8" + } }, - "eslint": { + "node_modules/eslint": { "version": "9.39.5", "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.5.tgz", "integrity": "sha512-DgZS62aPLXKlnxILS/AYCoRvHaZeXceIzlXPkkGGzJWSow1aEk0lbTlxUSlyjC8jcaKxAdOnTDz+o1JFSBsyjw==", "dev": true, - "requires": { + "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.2", @@ -2497,109 +4103,183 @@ "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, - "dependencies": { - "escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true - }, - "eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", - "dev": true - }, - "ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true } } }, - "eslint-scope": { + "node_modules/eslint-scope": { "version": "8.4.0", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", "dev": true, - "requires": { + "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, - "eslint-visitor-keys": { + "node_modules/eslint-visitor-keys": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", - "dev": true + "dev": true, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } }, - "espree": { + "node_modules/eslint/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/espree": { "version": "10.4.0", "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", "dev": true, - "requires": { + "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" }, - "dependencies": { - "eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", - "dev": true - } + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, - "esprima": { + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true + "dev": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } }, - "esquery": { + "node_modules/esquery": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", "dev": true, - "requires": { + "dependencies": { "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" } }, - "esrecurse": { + "node_modules/esrecurse": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", "dev": true, - "requires": { + "dependencies": { "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" } }, - "estraverse": { + "node_modules/estraverse": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true + "dev": true, + "engines": { + "node": ">=4.0" + } }, - "esutils": { + "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true + "dev": true, + "engines": { + "node": ">=0.10.0" + } }, - "etag": { + "node_modules/etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==" + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "engines": { + "node": ">= 0.6" + } }, - "execa": { + "node_modules/execa": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", "dev": true, - "requires": { + "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", "human-signals": "^2.1.0", @@ -2609,32 +4289,44 @@ "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" } }, - "exit": { + "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 + "dev": true, + "engines": { + "node": ">= 0.8.0" + } }, - "expect": { + "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, - "requires": { + "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" } }, - "express": { + "node_modules/express": { "version": "4.22.1", "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", - "requires": { + "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "~1.20.3", @@ -2667,104 +4359,132 @@ "utils-merge": "1.0.1", "vary": "~1.1.2" }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "requires": { - "ms": "2.0.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - }, - "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==" - } + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "fast-deep-equal": { + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/express/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==" + }, + "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true }, - "fast-json-stable-stringify": { + "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 }, - "fast-levenshtein": { + "node_modules/fast-levenshtein": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true }, - "fast-safe-stringify": { + "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==" }, - "fb-watchman": { + "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, - "requires": { + "dependencies": { "bser": "2.1.1" } }, - "fdir": { + "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true + "dev": true, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } }, - "fflate": { + "node_modules/fflate": { "version": "0.8.3", "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz", "integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==" }, - "file-entry-cache": { + "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", "dev": true, - "requires": { + "dependencies": { "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" } }, - "file-type": { + "node_modules/file-type": { "version": "20.4.1", "resolved": "https://registry.npmjs.org/file-type/-/file-type-20.4.1.tgz", "integrity": "sha512-hw9gNZXUfZ02Jo0uafWLaFVPter5/k2rfcrjFJJHX/77xtSDOfJuEFb6oKlFV86FLP1SuyHMW1PSk0U9M5tKkQ==", - "requires": { + "dependencies": { "@tokenizer/inflate": "^0.2.6", "strtok3": "^10.2.0", "token-types": "^6.0.0", "uint8array-extras": "^1.4.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" } }, - "fill-range": { + "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, - "requires": { + "dependencies": { "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" } }, - "finalhandler": { + "node_modules/finalhandler": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", - "requires": { + "dependencies": { "debug": "2.6.9", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", @@ -2773,122 +4493,181 @@ "statuses": "~2.0.2", "unpipe": "~1.0.0" }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "requires": { - "ms": "2.0.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - } + "engines": { + "node": ">= 0.8" } }, - "find-up": { + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", "dev": true, - "requires": { + "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "flat-cache": { + "node_modules/flat-cache": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", "dev": true, - "requires": { + "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" } }, - "flatted": { + "node_modules/flatted": { "version": "3.4.4", "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", "dev": true }, - "form-data": { + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", - "dev": true, - "requires": { + "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" } }, - "formidable": { + "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, - "requires": { + "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" } }, - "forwarded": { + "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==" + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "engines": { + "node": ">= 0.6" + } }, - "fresh": { + "node_modules/fresh": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==" + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "engines": { + "node": ">= 0.6" + } }, - "fs-minipass": { + "node_modules/fs-minipass": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", - "requires": { + "dependencies": { "minipass": "^3.0.0" }, - "dependencies": { - "minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "requires": { - "yallist": "^4.0.0" - } - } + "engines": { + "node": ">= 8" } }, - "fs.realpath": { + "node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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==" }, - "fsevents": { + "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, - "optional": true + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } }, - "function-bind": { + "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==" + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, - "gauge": { + "node_modules/gauge": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", - "requires": { + "deprecated": "This package is no longer supported.", + "dependencies": { "aproba": "^1.0.3 || ^2.0.0", "color-support": "^1.1.2", "console-control-strings": "^1.0.0", @@ -2898,25 +4677,34 @@ "string-width": "^4.2.3", "strip-ansi": "^6.0.1", "wide-align": "^1.1.2" + }, + "engines": { + "node": ">=10" } }, - "gensync": { + "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 + "dev": true, + "engines": { + "node": ">=6.9.0" + } }, - "get-caller-file": { + "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 + "dev": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } }, - "get-intrinsic": { + "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==", - "requires": { + "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", @@ -2927,395 +4715,597 @@ "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" } }, - "get-package-type": { + "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 + "dev": true, + "engines": { + "node": ">=8.0.0" + } }, - "get-proto": { + "node_modules/get-port": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/get-port/-/get-port-6.1.2.tgz", + "integrity": "sha512-BrGGraKm2uPqurfGVj/z97/zv8dPleC6x9JBNRTrDNtCkkRF4rPwrQXFgL7+I+q8QSdU4ntLQX2D7KIxSy8nGw==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "requires": { + "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" } }, - "get-stream": { + "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 + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, - "glob": { + "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "requires": { + "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", + "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" } }, - "glob-parent": { + "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", "dev": true, - "requires": { + "dependencies": { "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" } }, - "globals": { + "node_modules/globals": { "version": "16.5.0", "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz", "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==", - "dev": true + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, - "gopd": { + "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==" + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, - "graceful-fs": { + "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 }, - "handlebars": { + "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, - "requires": { + "dependencies": { "minimist": "^1.2.5", "neo-async": "^2.6.2", "source-map": "^0.6.1", - "uglify-js": "^3.1.4", "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" } }, - "has-flag": { + "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==" + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } }, - "has-symbols": { + "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==" + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, - "has-tostringtag": { + "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, - "requires": { + "dependencies": { "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "has-unicode": { + "node_modules/has-unicode": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==" }, - "hasown": { + "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==", - "requires": { + "dependencies": { "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" } }, - "html-escaper": { + "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 }, - "http-errors": { + "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==", - "requires": { + "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" } }, - "https-proxy-agent": { + "node_modules/https-proxy-agent": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", - "requires": { + "dependencies": { "agent-base": "6", "debug": "4" + }, + "engines": { + "node": ">= 6" } }, - "human-signals": { + "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 + "dev": true, + "engines": { + "node": ">=10.17.0" + } }, - "iconv-lite": { + "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==", - "requires": { + "dependencies": { "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" } }, - "ieee754": { + "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] }, - "ignore": { + "node_modules/ignore": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.6.tgz", "integrity": "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==", - "dev": true + "dev": true, + "engines": { + "node": ">= 4" + } }, - "import-fresh": { + "node_modules/import-fresh": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", "dev": true, - "requires": { + "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "import-local": { + "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, - "requires": { + "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" } }, - "imurmurhash": { + "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true + "dev": true, + "engines": { + "node": ">=0.8.19" + } }, - "inflight": { + "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==", - "requires": { + "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.", + "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, - "inherits": { + "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, - "ipaddr.js": { + "node_modules/ioredis": { + "version": "5.11.1", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.11.1.tgz", + "integrity": "sha512-ehuGcf94bQXhfagULNXrJdfnWO38v070jxSx/qE87Kjzmu2fU7ro5EFAb+OPituLqgfyuQaym5DlrNydW2sJ9A==", + "license": "MIT", + "dependencies": { + "@ioredis/commands": "1.10.0", + "cluster-key-slot": "1.1.1", + "debug": "4.4.3", + "denque": "2.1.0", + "redis-errors": "1.2.0", + "redis-parser": "3.0.0", + "standard-as-callback": "2.1.0" + }, + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ioredis" + } + }, + "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==" + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "engines": { + "node": ">= 0.10" + } }, - "is-arrayish": { + "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 }, - "is-binary-path": { + "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, - "requires": { + "dependencies": { "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" } }, - "is-core-module": { + "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, - "requires": { + "dependencies": { "hasown": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "is-extglob": { + "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 + "dev": true, + "engines": { + "node": ">=0.10.0" + } }, - "is-fullwidth-code-point": { + "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==" + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } }, - "is-generator-fn": { + "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 + "dev": true, + "engines": { + "node": ">=6" + } }, - "is-glob": { + "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, - "requires": { + "dependencies": { "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" } }, - "is-number": { + "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 + "dev": true, + "engines": { + "node": ">=0.12.0" + } }, - "is-stream": { + "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 + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, - "isexe": { + "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true }, - "istanbul-lib-coverage": { + "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 + "dev": true, + "engines": { + "node": ">=8" + } }, - "istanbul-lib-instrument": { + "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, - "requires": { + "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" } }, - "istanbul-lib-report": { + "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, - "requires": { + "dependencies": { "istanbul-lib-coverage": "^3.0.0", "make-dir": "^4.0.0", "supports-color": "^7.1.0" }, - "dependencies": { - "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, - "requires": { - "semver": "^7.5.3" - } - } + "engines": { + "node": ">=10" } }, - "istanbul-lib-source-maps": { + "node_modules/istanbul-lib-report/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, + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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, - "requires": { + "dependencies": { "debug": "^4.1.1", "istanbul-lib-coverage": "^3.0.0", "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" } }, - "istanbul-reports": { + "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, - "requires": { + "dependencies": { "html-escaper": "^2.0.0", "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" } }, - "iterare": { + "node_modules/iterare": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/iterare/-/iterare-1.2.1.tgz", - "integrity": "sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q==" + "integrity": "sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q==", + "engines": { + "node": ">=6" + } }, - "jest": { + "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, - "requires": { + "dependencies": { "@jest/core": "^29.7.0", "@jest/types": "^29.6.3", "import-local": "^3.0.2", "jest-cli": "^29.7.0" }, - "dependencies": { - "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, - "requires": { - "@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 } } }, - "jest-changed-files": { + "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, - "requires": { + "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" } }, - "jest-circus": { + "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, - "requires": { + "dependencies": { "@jest/environment": "^29.7.0", "@jest/expect": "^29.7.0", "@jest/test-result": "^29.7.0", @@ -3336,14 +5326,17 @@ "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" } }, - "jest-config": { + "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, - "requires": { + "dependencies": { "@babel/core": "^7.11.6", "@jest/test-sequencer": "^29.7.0", "@jest/types": "^29.6.3", @@ -3366,110 +5359,151 @@ "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 + } } }, - "jest-diff": { + "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, - "requires": { + "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" } }, - "jest-docblock": { + "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, - "requires": { + "dependencies": { "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "jest-each": { + "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, - "requires": { + "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" } }, - "jest-environment-node": { + "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, - "requires": { + "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" } }, - "jest-get-type": { + "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 + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } }, - "jest-haste-map": { + "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, - "requires": { + "dependencies": { "@jest/types": "^29.6.3", "@types/graceful-fs": "^4.1.3", "@types/node": "*", "anymatch": "^3.0.3", "fb-watchman": "^2.0.0", - "fsevents": "^2.3.2", "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" } }, - "jest-leak-detector": { + "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, - "requires": { + "dependencies": { "jest-get-type": "^29.6.3", "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "jest-matcher-utils": { + "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, - "requires": { + "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" } }, - "jest-message-util": { + "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, - "requires": { + "dependencies": { "@babel/code-frame": "^7.12.13", "@jest/types": "^29.6.3", "@types/stack-utils": "^2.0.0", @@ -3479,37 +5513,57 @@ "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" } }, - "jest-mock": { + "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, - "requires": { + "dependencies": { "@jest/types": "^29.6.3", "@types/node": "*", "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "jest-pnp-resolver": { + "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 + "dev": true, + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } }, - "jest-regex-util": { + "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 + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } }, - "jest-resolve": { + "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, - "requires": { + "dependencies": { "chalk": "^4.0.0", "graceful-fs": "^4.2.9", "jest-haste-map": "^29.7.0", @@ -3519,24 +5573,30 @@ "resolve": "^1.20.0", "resolve.exports": "^2.0.0", "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "jest-resolve-dependencies": { + "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, - "requires": { + "dependencies": { "jest-regex-util": "^29.6.3", "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "jest-runner": { + "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, - "requires": { + "dependencies": { "@jest/console": "^29.7.0", "@jest/environment": "^29.7.0", "@jest/test-result": "^29.7.0", @@ -3558,14 +5618,17 @@ "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" } }, - "jest-runtime": { + "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, - "requires": { + "dependencies": { "@jest/environment": "^29.7.0", "@jest/fake-timers": "^29.7.0", "@jest/globals": "^29.7.0", @@ -3588,14 +5651,17 @@ "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" } }, - "jest-snapshot": { + "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, - "requires": { + "dependencies": { "@babel/core": "^7.11.6", "@babel/generator": "^7.7.2", "@babel/plugin-syntax-jsx": "^7.7.2", @@ -3616,28 +5682,47 @@ "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" } }, - "jest-util": { + "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, - "requires": { + "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" } }, - "jest-validate": { + "node_modules/jest-util/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/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, - "requires": { + "dependencies": { "@jest/types": "^29.6.3", "camelcase": "^6.2.0", "chalk": "^4.0.0", @@ -3645,21 +5730,28 @@ "leven": "^3.1.0", "pretty-format": "^29.7.0" }, - "dependencies": { - "camelcase": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", - "dev": true - } + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "jest-watcher": { + "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, + "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, - "requires": { + "dependencies": { "@jest/test-result": "^29.7.0", "@jest/types": "^29.6.3", "@types/node": "*", @@ -3668,86 +5760,153 @@ "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" } }, - "jest-worker": { + "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, - "requires": { + "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, "dependencies": { - "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, - "requires": { - "has-flag": "^4.0.0" - } + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/jest/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, + "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 } } }, - "js-tokens": { + "node_modules/jose": { + "version": "6.2.6", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.6.tgz", + "integrity": "sha512-HwMtbJjMw8rC8dUTwCNilHJD+fxTeKM3JV1eprSmTjS41qwXSSt6exJXgyPK1QOu0jB9eDYLESRDkB3qaT3jnw==", + "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 }, - "js-yaml": { + "node_modules/js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "requires": { + "dependencies": { "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" } }, - "jsesc": { + "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "dev": true + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } }, - "json-buffer": { + "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", "dev": true }, - "json-parse-even-better-errors": { + "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 }, - "json-schema-traverse": { + "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "dev": true }, - "json-stable-stringify-without-jsonify": { + "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true }, - "json5": { + "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 + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } }, - "jsonwebtoken": { + "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==", - "requires": { + "dependencies": { "jws": "^4.0.1", "lodash.includes": "^4.3.0", "lodash.isboolean": "^3.0.3", @@ -3759,312 +5918,433 @@ "ms": "^2.1.1", "semver": "^7.5.4" }, - "dependencies": { - "jwa": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", - "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", - "requires": { - "buffer-equal-constant-time": "^1.0.1", - "ecdsa-sig-formatter": "1.0.11", - "safe-buffer": "^5.0.1" - } - }, - "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==", - "requires": { - "jwa": "^2.0.1", - "safe-buffer": "^5.0.1" - } - } + "engines": { + "node": ">=12", + "npm": ">=6" } }, - "jwa": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz", - "integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==", - "requires": { + "node_modules/jsonwebtoken/node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, - "jws": { + "node_modules/jsonwebtoken/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==", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jwa": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz", + "integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.3.tgz", "integrity": "sha512-byiJ0FLRdLdSVSReO/U4E7RoEyOCKnEnEPMjq3HxWtvzLsV08/i5RQKsFVNkCldrCaPr2vDNAOMsfs8T/Hze7g==", - "requires": { + "dependencies": { "jwa": "^1.4.2", "safe-buffer": "^5.0.1" } }, - "keyv": { + "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", "dev": true, - "requires": { + "dependencies": { "json-buffer": "3.0.1" } }, - "kleur": { + "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 + "dev": true, + "engines": { + "node": ">=6" + } }, - "leven": { + "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 + "dev": true, + "engines": { + "node": ">=6" + } }, - "levn": { + "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", "dev": true, - "requires": { + "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" } }, - "libphonenumber-js": { + "node_modules/libphonenumber-js": { "version": "1.13.10", "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.13.10.tgz", "integrity": "sha512-xJxrdqvbl2rtn2MaUJrUejz8J7/uZNC0V77oks2LxYrO/+ZtVpRmz+fEQMuu6VusnEB1fmpByiLS1WXecOnAnw==" }, - "lines-and-columns": { + "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 }, - "locate-path": { + "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", "dev": true, - "requires": { + "dependencies": { "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "lodash": { + "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, - "lodash.includes": { + "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==" }, - "lodash.isboolean": { + "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==" }, - "lodash.isinteger": { + "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==" }, - "lodash.isnumber": { + "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==" }, - "lodash.isplainobject": { + "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==" }, - "lodash.isstring": { + "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==" }, - "lodash.memoize": { + "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 }, - "lodash.merge": { + "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, - "lodash.once": { + "node_modules/lodash.once": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" }, - "lru-cache": { + "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, - "requires": { - "yallist": "^3.0.2" - }, "dependencies": { - "yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true - } + "yallist": "^3.0.2" } }, - "make-dir": { + "node_modules/lru-cache/node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, + "node_modules/luxon": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.2.tgz", + "integrity": "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/make-dir": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", - "requires": { + "dependencies": { "semver": "^6.0.0" }, - "dependencies": { - "semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==" - } + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "make-error": { + "node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "bin": { + "semver": "bin/semver.js" + } + }, + "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 }, - "makeerror": { + "node_modules/makeerror": { "version": "1.0.12", "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", "dev": true, - "requires": { + "dependencies": { "tmpl": "1.0.5" } }, - "math-intrinsics": { + "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==" + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "engines": { + "node": ">= 0.4" + } }, - "media-typer": { + "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==" + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "engines": { + "node": ">= 0.6" + } }, - "merge-descriptors": { + "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==" + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, - "merge-stream": { + "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 }, - "methods": { + "node_modules/methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==" + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "engines": { + "node": ">= 0.6" + } }, - "micromatch": { + "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, - "requires": { + "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" } }, - "mime": { + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/mime": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } }, - "mime-db": { + "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==" + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } }, - "mime-types": { + "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==", - "requires": { + "dependencies": { "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" } }, - "mimic-fn": { + "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 + "dev": true, + "engines": { + "node": ">=6" + } }, - "minimatch": { + "node_modules/minimatch": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "requires": { + "dependencies": { "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" } }, - "minimist": { + "node_modules/minimist": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==" + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, - "minipass": { + "node_modules/minipass": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", - "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==" + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "engines": { + "node": ">=8" + } }, - "minizlib": { + "node_modules/minizlib": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", - "requires": { + "dependencies": { "minipass": "^3.0.0", "yallist": "^4.0.0" }, - "dependencies": { - "minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "requires": { - "yallist": "^4.0.0" - } - } + "engines": { + "node": ">= 8" } }, - "mkdirp": { + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mkdirp": { "version": "0.5.6", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "requires": { + "dependencies": { "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" } }, - "ms": { + "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==" }, - "multer": { + "node_modules/msgpackr": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.12.1.tgz", + "integrity": "sha512-4EUH9tQHnMmEgzW/MdAP0KIfa1T9AF+htl0ffe2n5vb2EKn9y2co8ccpgWko6S52Jy1PQZKwRnx5/KkYjtd9MQ==", + "license": "MIT", + "optionalDependencies": { + "msgpackr-extract": "^3.0.2" + } + }, + "node_modules/msgpackr-extract": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.4.tgz", + "integrity": "sha512-4kmO/MdyUIkLIvTPr8VHLil4AtoKIoniWPIEk5+CDy0xnWC84azhSFmuJ7PxZdsYtiP5kEeQsORAVIeMgxT+Hw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "node-gyp-build-optional-packages": "5.2.2" + }, + "bin": { + "download-msgpackr-prebuilds": "bin/download-prebuilds.js" + }, + "optionalDependencies": { + "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.4", + "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.4", + "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.4", + "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.4", + "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.4", + "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.4" + } + }, + "node_modules/multer": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/multer/-/multer-2.0.2.tgz", "integrity": "sha512-u7f2xaZ/UG8oLXHvtF/oWTRvT44p9ecwBBqTwgJVq0+4BW1g8OW01TyMEGWBHbyMOYVHXslaut7qEQ1meATXgw==", - "requires": { + "dependencies": { "append-field": "^1.0.0", "busboy": "^1.6.0", "concat-stream": "^2.0.0", @@ -4072,262 +6352,418 @@ "object-assign": "^4.1.1", "type-is": "^1.6.18", "xtend": "^4.0.2" + }, + "engines": { + "node": ">= 10.16.0" } }, - "natural-compare": { + "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 }, - "negotiator": { + "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==" + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "engines": { + "node": ">= 0.6" + } }, - "neo-async": { + "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 }, - "node-addon-api": { + "node_modules/node-addon-api": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==" }, - "node-fetch": { + "node_modules/node-fetch": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "requires": { + "dependencies": { "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } } }, - "node-int64": { + "node_modules/node-gyp-build-optional-packages": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz", + "integrity": "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==", + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.1" + }, + "bin": { + "node-gyp-build-optional-packages": "bin.js", + "node-gyp-build-optional-packages-optional": "optional.js", + "node-gyp-build-optional-packages-test": "build-test.js" + } + }, + "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 }, - "node-releases": { + "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 + "dev": true, + "engines": { + "node": ">=18" + } }, - "nopt": { + "node_modules/nodemailer": { + "version": "6.10.1", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.10.1.tgz", + "integrity": "sha512-Z+iLaBGVaSjbIzQ4pX6XV41HrooLsQ10ZWPUehGmuantvzWoDVBnmsdUcOIDM1t+yPor5pDhVlDESgOMEGxhHA==", + "license": "MIT-0", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/nopt": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", - "requires": { + "dependencies": { "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" } }, - "normalize-path": { + "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 + "dev": true, + "engines": { + "node": ">=0.10.0" + } }, - "npm-run-path": { + "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, - "requires": { + "dependencies": { "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" } }, - "npmlog": { + "node_modules/npmlog": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", - "requires": { + "deprecated": "This package is no longer supported.", + "dependencies": { "are-we-there-yet": "^2.0.0", "console-control-strings": "^1.1.0", "gauge": "^3.0.0", "set-blocking": "^2.0.0" } }, - "object-assign": { + "node_modules/oauth4webapi": { + "version": "3.8.6", + "resolved": "https://registry.npmjs.org/oauth4webapi/-/oauth4webapi-3.8.6.tgz", + "integrity": "sha512-iwemM91xz8nryHti2yTmg5fhyEMVOkOXwHNqbvcATjyajb5oQxCQzrNOA6uElRHuMhQQTKUyFKV9y/CNyg25BQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "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==" + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } }, - "object-inspect": { + "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==" + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, - "on-exit-leak-free": { + "node_modules/on-exit-leak-free": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", - "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==" + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "engines": { + "node": ">=14.0.0" + } }, - "on-finished": { + "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==", - "requires": { + "dependencies": { "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" } }, - "once": { + "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==", - "requires": { + "dependencies": { "wrappy": "1" } }, - "onetime": { + "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, - "requires": { + "dependencies": { "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "optionator": { + "node_modules/openid-client": { + "version": "6.8.4", + "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-6.8.4.tgz", + "integrity": "sha512-QSw0BA08piujetEwfZsHoTrDpMEha7GDZDicQqVwX4u0ChCjefvjDB++TZ8BTg76UpwhzIQgdvvfgfl3HpCSAw==", + "license": "MIT", + "dependencies": { + "jose": "^6.2.2", + "oauth4webapi": "^3.8.5" + }, + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", "dev": true, - "requires": { + "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" } }, - "p-limit": { + "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, - "requires": { + "dependencies": { "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "p-locate": { + "node_modules/p-locate": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", "dev": true, - "requires": { + "dependencies": { "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "p-try": { + "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 + "dev": true, + "engines": { + "node": ">=6" + } }, - "parent-module": { + "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", "dev": true, - "requires": { + "dependencies": { "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" } }, - "parse-json": { + "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, - "requires": { + "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" } }, - "parseurl": { + "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } }, - "passport": { + "node_modules/passport": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/passport/-/passport-0.7.0.tgz", "integrity": "sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==", - "requires": { + "dependencies": { "passport-strategy": "1.x.x", "pause": "0.0.1", "utils-merge": "^1.0.1" + }, + "engines": { + "node": ">= 0.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jaredhanson" } }, - "passport-jwt": { + "node_modules/passport-jwt": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/passport-jwt/-/passport-jwt-4.0.1.tgz", "integrity": "sha512-UCKMDYhNuGOBE9/9Ycuoyh7vP6jpeTp/+sfMJl7nLff/t6dps+iaeE0hhNkKN8/HZHcJ7lCdOyDxHdDoxoSvdQ==", - "requires": { + "dependencies": { "jsonwebtoken": "^9.0.0", "passport-strategy": "^1.0.0" } }, - "passport-strategy": { + "node_modules/passport-strategy": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", - "integrity": "sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==" + "integrity": "sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==", + "engines": { + "node": ">= 0.4.0" + } }, - "path-exists": { + "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 + "dev": true, + "engines": { + "node": ">=8" + } }, - "path-is-absolute": { + "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==" + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "engines": { + "node": ">=0.10.0" + } }, - "path-key": { + "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 + "dev": true, + "engines": { + "node": ">=8" + } }, - "path-parse": { + "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 }, - "path-to-regexp": { + "node_modules/path-to-regexp": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-3.3.0.tgz", "integrity": "sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw==" }, - "pause": { + "node_modules/pause": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==" }, - "picocolors": { + "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 }, - "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 + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } }, - "pino": { + "node_modules/pino": { "version": "9.14.0", "resolved": "https://registry.npmjs.org/pino/-/pino-9.14.0.tgz", "integrity": "sha512-8OEwKp5juEvb/MjpIc4hjqfgCNysrS94RIOMXYvpYCdm/jglrKEiAYmiumbmGhCvs+IcInsphYDFwqrjr7398w==", - "requires": { + "dependencies": { "@pinojs/redact": "^0.4.0", "atomic-sleep": "^1.0.0", "on-exit-leak-free": "^2.1.0", @@ -4339,304 +6775,546 @@ "safe-stable-stringify": "^2.3.1", "sonic-boom": "^4.0.1", "thread-stream": "^3.0.0" + }, + "bin": { + "pino": "bin.js" } }, - "pino-abstract-transport": { + "node_modules/pino-abstract-transport": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz", "integrity": "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==", - "requires": { + "dependencies": { "split2": "^4.0.0" } }, - "pino-std-serializers": { + "node_modules/pino-std-serializers": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz", "integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==" }, - "pirates": { + "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 + "dev": true, + "engines": { + "node": ">= 6" + } }, - "pkg-dir": { + "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, - "requires": { + "dependencies": { "find-up": "^4.0.0" }, - "dependencies": { - "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, - "requires": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - } - }, - "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, - "requires": { - "p-locate": "^4.1.0" - } - }, - "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, - "requires": { - "p-try": "^2.0.0" - } - }, - "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, - "requires": { - "p-limit": "^2.2.0" - } - } + "engines": { + "node": ">=8" } }, - "prelude-ls": { + "node_modules/pkg-dir/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/playwright": { + "version": "1.62.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.62.1.tgz", + "integrity": "sha512-0M+L3LAD8/nm554LOla9Ayx0j0tmFZ0FBcoQ7F1VuVHpM/XpiC8RcDzBQB8W5+hA8L22THxELzeF+2WcUzvcLg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.62.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=20" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.62.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.62.1.tgz", + "integrity": "sha512-wPYSwEBJY9GHraISXqyqtx0na0LpO3XEX7jNDhntbex7tzUS7kLnZsOlFruFJB4Hi/rhDMjXGqHewDZ68nYZVw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true + "dev": true, + "engines": { + "node": ">= 0.8.0" + } }, - "prettier": { + "node_modules/prettier": { "version": "3.9.6", "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.6.tgz", "integrity": "sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==", - "dev": true + "dev": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } }, - "pretty-format": { + "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, - "requires": { + "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" }, - "dependencies": { - "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 - } + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "prisma": { + "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, + "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==", - "dev": true, - "requires": { - "@prisma/engines": "5.22.0", + "devOptional": true, + "hasInstallScript": true, + "dependencies": { + "@prisma/engines": "5.22.0" + }, + "bin": { + "prisma": "build/index.js" + }, + "engines": { + "node": ">=16.13" + }, + "optionalDependencies": { "fsevents": "2.3.3" } }, - "process-warning": { + "node_modules/process-warning": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.1.0.tgz", - "integrity": "sha512-jQSaVHsPgtyw60e1rQ/A+/ArPEj/S8pS/vFnyGa/gYFXrKk/6RuDkoqVDQ5NI5MmS01698ltlAk0NoDBNLujRw==" + "integrity": "sha512-jQSaVHsPgtyw60e1rQ/A+/ArPEj/S8pS/vFnyGa/gYFXrKk/6RuDkoqVDQ5NI5MmS01698ltlAk0NoDBNLujRw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ] }, - "prompts": { + "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, - "requires": { + "dependencies": { "kleur": "^3.0.3", "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" } }, - "proxy-addr": { + "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==", - "requires": { + "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" } }, - "punycode": { + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true + "dev": true, + "engines": { + "node": ">=6" + } }, - "pure-rand": { + "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 + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ] }, - "qs": { + "node_modules/qs": { "version": "6.14.2", "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", - "requires": { + "dependencies": { "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "quick-format-unescaped": { + "node_modules/quick-format-unescaped": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==" }, - "range-parser": { + "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==" + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "engines": { + "node": ">= 0.6" + } }, - "raw-body": { + "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==", - "requires": { + "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" } }, - "react-is": { + "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 }, - "readable-stream": { + "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "requires": { + "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" } }, - "readdirp": { + "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", "dev": true, - "requires": { + "dependencies": { "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" } }, - "real-require": { + "node_modules/readdirp/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/real-require": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", - "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==" + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "engines": { + "node": ">= 12.13.0" + } }, - "reflect-metadata": { + "node_modules/redis-errors": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", + "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/redis-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", + "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==", + "license": "MIT", + "dependencies": { + "redis-errors": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/reflect-metadata": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==" }, - "require-directory": { + "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 + "dev": true, + "engines": { + "node": ">=0.10.0" + } }, - "resolve": { + "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, - "requires": { + "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" } }, - "resolve-cwd": { + "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, - "requires": { + "dependencies": { "resolve-from": "^5.0.0" }, - "dependencies": { - "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 - } + "engines": { + "node": ">=8" } }, - "resolve-from": { + "node_modules/resolve-cwd/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true + "dev": true, + "engines": { + "node": ">=4" + } }, - "resolve.exports": { + "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 + "dev": true, + "engines": { + "node": ">=10" + } }, - "rimraf": { + "node_modules/rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "requires": { + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dependencies": { "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "rxjs": { + "node_modules/rxjs": { "version": "7.8.2", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", - "requires": { + "dependencies": { "tslib": "^2.1.0" } }, - "safe-buffer": { + "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==" + "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" + } + ] }, - "safe-stable-stringify": { + "node_modules/safe-stable-stringify": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", - "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==" + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "engines": { + "node": ">=10" + } }, - "safer-buffer": { + "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, - "semver": { + "node_modules/semver": { "version": "7.8.5", "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", - "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==" + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } }, - "send": { + "node_modules/send": { "version": "0.19.2", "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", - "requires": { + "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", @@ -4651,243 +7329,327 @@ "range-parser": "~1.2.1", "statuses": "~2.0.2" }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "requires": { - "ms": "2.0.0" - }, - "dependencies": { - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - } - } - } + "engines": { + "node": ">= 0.8.0" } }, - "serve-static": { + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/serve-static": { "version": "1.16.3", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", - "requires": { + "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" } }, - "set-blocking": { + "node_modules/set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" }, - "setprototypeof": { + "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" }, - "shebang-command": { + "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, - "requires": { + "dependencies": { "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" } }, - "shebang-regex": { + "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 + "dev": true, + "engines": { + "node": ">=8" + } }, - "side-channel": { + "node_modules/side-channel": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", - "requires": { + "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" } }, - "side-channel-list": { + "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==", - "requires": { + "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "side-channel-map": { + "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==", - "requires": { + "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" } }, - "side-channel-weakmap": { + "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==", - "requires": { + "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" } }, - "signal-exit": { + "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==" }, - "sisteransi": { + "node_modules/sisteransi": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", "dev": true }, - "slash": { + "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true + "dev": true, + "engines": { + "node": ">=8" + } }, - "sonic-boom": { + "node_modules/sonic-boom": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz", "integrity": "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==", - "requires": { + "dependencies": { "atomic-sleep": "^1.0.0" } }, - "source-map": { + "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 + "dev": true, + "engines": { + "node": ">=0.10.0" + } }, - "source-map-support": { + "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, - "requires": { + "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, - "split2": { + "node_modules/split2": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", - "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==" + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "engines": { + "node": ">= 10.x" + } }, - "sprintf-js": { + "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 }, - "stack-utils": { + "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, - "requires": { + "dependencies": { "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" } }, - "statuses": { + "node_modules/standard-as-callback": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", + "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==", + "license": "MIT" + }, + "node_modules/statuses": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", - "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==" + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "engines": { + "node": ">= 0.8" + } }, - "streamsearch": { + "node_modules/streamsearch": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", - "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==" + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } }, - "string-length": { + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-length": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", "dev": true, - "requires": { + "dependencies": { "char-regex": "^1.0.2", "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" } }, - "string-width": { + "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==", - "requires": { + "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" } }, - "string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "requires": { - "safe-buffer": "~5.2.0" - } - }, - "strip-ansi": { + "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==", - "requires": { + "dependencies": { "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" } }, - "strip-bom": { + "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 + "dev": true, + "engines": { + "node": ">=8" + } }, - "strip-final-newline": { + "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 + "dev": true, + "engines": { + "node": ">=6" + } }, - "strip-json-comments": { + "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 + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, - "strtok3": { + "node_modules/strtok3": { "version": "10.3.5", "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.3.5.tgz", "integrity": "sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA==", - "requires": { + "dependencies": { "@tokenizer/token": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" } }, - "superagent": { + "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, - "requires": { + "dependencies": { "component-emitter": "^1.3.1", "cookiejar": "^2.1.4", "debug": "^4.3.7", @@ -4898,66 +7660,93 @@ "mime": "2.6.0", "qs": "^6.14.1" }, - "dependencies": { - "mime": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", - "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", - "dev": true - } + "engines": { + "node": ">=14.18.0" } }, - "supertest": { + "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, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/supertest": { "version": "7.2.2", "resolved": "https://registry.npmjs.org/supertest/-/supertest-7.2.2.tgz", "integrity": "sha512-oK8WG9diS3DlhdUkcFn4tkNIiIbBx9lI2ClF8K+b2/m8Eyv47LSawxUzZQSNKUrVb2KsqeTDCcjAAVPYaSLVTA==", "dev": true, - "requires": { + "dependencies": { "cookie-signature": "^1.2.2", "methods": "^1.1.2", "superagent": "^10.3.0" }, - "dependencies": { - "cookie-signature": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", - "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", - "dev": true - } + "engines": { + "node": ">=14.18.0" } }, - "supports-color": { + "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, + "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==", - "requires": { + "dependencies": { "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" } }, - "supports-preserve-symlinks-flag": { + "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 + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, - "swagger-ui-dist": { + "node_modules/swagger-ui-dist": { "version": "5.17.14", "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.17.14.tgz", "integrity": "sha512-CVbSfaLpstV65OnSjbXfVd6Sta3q3F7Cj/yYuvHMp1P90LztOLs6PfUnKEVAeiIVQt9u2SaPwv0LiH/OyMjHRw==" }, - "swagger-ui-express": { + "node_modules/swagger-ui-express": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-5.0.1.tgz", "integrity": "sha512-SrNU3RiBGTLLmFU8GIJdOdanJTl4TOmT27tt3bWWHppqYmAZ6IDuEuBvMU6nZq0zLEe6b/1rACXCgLZqO6ZfrA==", - "requires": { + "dependencies": { "swagger-ui-dist": ">=5.0.0" + }, + "engines": { + "node": ">= v0.10.32" + }, + "peerDependencies": { + "express": ">=4.0.0 || >=5.0.0-beta" } }, - "tar": { + "node_modules/tar": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", - "requires": { + "deprecated": "Old versions of tar 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", + "dependencies": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", "minipass": "^5.0.0", @@ -4965,104 +7754,134 @@ "mkdirp": "^1.0.3", "yallist": "^4.0.0" }, - "dependencies": { - "mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==" - } + "engines": { + "node": ">=10" } }, - "test-exclude": { + "node_modules/tar/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "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, - "requires": { + "dependencies": { "@istanbuljs/schema": "^0.1.2", "glob": "^7.1.4", "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" } }, - "thread-stream": { + "node_modules/thread-stream": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.2.0.tgz", "integrity": "sha512-zLBvqpwr4Esa0kRjcrzGU6zL25lePWaCLMx0RQFrmteozIfeNdaMLpG5U7PeHzvlFkAWaRKA9/KVW4F60iB+qw==", - "requires": { + "dependencies": { "real-require": "^0.2.0" } }, - "tinyglobby": { + "node_modules/tinyglobby": { "version": "0.2.17", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", "dev": true, - "requires": { + "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" }, - "dependencies": { - "picomatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", - "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", - "dev": true - } + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" } }, - "tmpl": { + "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 }, - "to-regex-range": { + "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, - "requires": { + "dependencies": { "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" } }, - "toidentifier": { + "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==" + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "engines": { + "node": ">=0.6" + } }, - "token-types": { + "node_modules/token-types": { "version": "6.1.2", "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.1.2.tgz", "integrity": "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==", - "requires": { + "dependencies": { "@borewit/text-codec": "^0.2.1", "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" } }, - "tr46": { + "node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" }, - "tree-kill": { + "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 + "dev": true, + "bin": { + "tree-kill": "cli.js" + } }, - "ts-api-utils": { + "node_modules/ts-api-utils": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", - "dev": true + "dev": true, + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } }, - "ts-jest": { + "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, - "requires": { + "dependencies": { "bs-logger": "^0.2.6", "fast-json-stable-stringify": "^2.1.0", "handlebars": "^4.7.9", @@ -5073,21 +7892,61 @@ "type-fest": "^4.41.0", "yargs-parser": "^21.1.1" }, - "dependencies": { - "type-fest": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", - "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", - "dev": true + "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 } } }, - "ts-node": { + "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, + "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, - "requires": { + "license": "MIT", + "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", "@tsconfig/node12": "^1.0.7", @@ -5101,14 +7960,36 @@ "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 + } } }, - "ts-node-dev": { + "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, - "requires": { + "dependencies": { "chokidar": "^3.5.1", "dynamic-dedupe": "^0.3.0", "minimist": "^1.2.6", @@ -5120,287 +8001,419 @@ "ts-node": "^10.4.0", "tsconfig": "^7.0.0" }, - "dependencies": { - "mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "dev": true - }, - "rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } + "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 } } }, - "tsconfig": { + "node_modules/ts-node-dev/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ts-node-dev/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, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "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, - "requires": { + "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" - }, - "dependencies": { - "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 - }, - "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 - } } }, - "tslib": { + "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, + "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, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" }, - "type-check": { + "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", "dev": true, - "requires": { + "dependencies": { "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" } }, - "type-detect": { + "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 + "dev": true, + "engines": { + "node": ">=4" + } }, - "type-fest": { + "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 + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, - "type-is": { + "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==", - "requires": { + "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" } }, - "typedarray": { + "node_modules/typedarray": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" }, - "typescript": { + "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } }, - "uglify-js": { + "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, - "optional": true + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } }, - "uid": { + "node_modules/uid": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/uid/-/uid-2.0.2.tgz", "integrity": "sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g==", - "requires": { + "dependencies": { "@lukeed/csprng": "^1.0.0" + }, + "engines": { + "node": ">=8" } }, - "uint8array-extras": { + "node_modules/uint8array-extras": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.5.0.tgz", - "integrity": "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==" + "integrity": "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, - "undici-types": { + "node_modules/undici-types": { "version": "8.3.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==" }, - "unpipe": { + "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==" + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "engines": { + "node": ">= 0.8" + } }, - "update-browserslist-db": { + "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, - "requires": { + "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" + } + ], + "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" } }, - "uri-js": { + "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", "dev": true, - "requires": { + "dependencies": { "punycode": "^2.1.0" } }, - "util-deprecate": { + "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, - "utils-merge": { + "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==" + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "engines": { + "node": ">= 0.4.0" + } }, - "uuid": { + "node_modules/uuid": { "version": "11.1.1", "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.1.tgz", - "integrity": "sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==" + "integrity": "sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/esm/bin/uuid" + } }, - "v8-compile-cache-lib": { + "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 }, - "v8-to-istanbul": { + "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, - "requires": { + "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" } }, - "validator": { + "node_modules/validator": { "version": "13.15.35", "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.35.tgz", - "integrity": "sha512-TQ5pAGhd5whStmqWvYF4OjQROlmv9SMFVt37qoCBdqRffuuklWYQlCNnEs2ZaIBD1kZRNnikiZOS1eqgkar0iw==" + "integrity": "sha512-TQ5pAGhd5whStmqWvYF4OjQROlmv9SMFVt37qoCBdqRffuuklWYQlCNnEs2ZaIBD1kZRNnikiZOS1eqgkar0iw==", + "engines": { + "node": ">= 0.10" + } }, - "vary": { + "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==" + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "engines": { + "node": ">= 0.8" + } }, - "walker": { + "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, - "requires": { + "dependencies": { "makeerror": "1.0.12" } }, - "webidl-conversions": { + "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" }, - "whatwg-url": { + "node_modules/whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "requires": { + "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, - "which": { + "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, - "requires": { + "dependencies": { "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" } }, - "wide-align": { + "node_modules/wide-align": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", - "requires": { + "dependencies": { "string-width": "^1.0.2 || 2 || 3 || 4" } }, - "word-wrap": { + "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", - "dev": true + "dev": true, + "engines": { + "node": ">=0.10.0" + } }, - "wordwrap": { + "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 }, - "wrap-ansi": { + "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, - "requires": { + "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" } }, - "wrappy": { + "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==" }, - "write-file-atomic": { + "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, - "requires": { + "dependencies": { "imurmurhash": "^0.1.4", "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, - "xtend": { + "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "engines": { + "node": ">=0.4" + } }, - "y18n": { + "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 + "dev": true, + "engines": { + "node": ">=10" + } }, - "yallist": { + "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, - "yargs": { + "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, - "requires": { + "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", @@ -5408,25 +8421,40 @@ "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" } }, - "yargs-parser": { + "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 + "dev": true, + "engines": { + "node": ">=12" + } }, - "yn": { + "node_modules/yn": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", - "dev": true + "dev": true, + "engines": { + "node": ">=6" + } }, - "yocto-queue": { + "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 + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } } } } diff --git a/package.json b/package.json index 48b8469..9558769 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "scripts": { "start": "node dist/main.js", "start:dev": "ts-node-dev --respawn --pretty --transpile-only src/main.ts", + "start:debug": "nest start --debug --watch", "build": "tsc -p tsconfig.json", "lint": "eslint \"src/**/*.ts\"", "format": "prettier --write \"src/**/*.ts\" \"tests/**/*.ts\"", @@ -24,6 +25,7 @@ "prisma:seed": "ts-node prisma/seed.ts" }, "dependencies": { + "@aws-sdk/client-s3": "^3.379.0", "@nestjs/common": "^10.4.20", "@nestjs/config": "^3.3.0", "@nestjs/core": "^10.4.20", @@ -33,11 +35,18 @@ "@nestjs/platform-express": "^10.4.20", "@nestjs/swagger": "^7.4.2", "@prisma/client": "^5.22.0", + "axios": "^1.4.0", "bcrypt": "^5.1.1", + "bullmq": "^1.73.0", "class-transformer": "^0.5.1", "class-validator": "^0.14.2", + "cookie-parser": "^1.4.7", "dotenv": "^16.6.1", + "ioredis": "^5.3.2", + "jose": "^6.2.6", "jsonwebtoken": "^9.0.2", + "nodemailer": "^6.9.4", + "openid-client": "^6.5.0", "passport": "^0.7.0", "passport-jwt": "^4.0.1", "pino": "^9.7.0", @@ -47,16 +56,19 @@ "uuid": "^11.1.0" }, "devDependencies": { + "@eslint/js": "^9.21.0", + "@nestjs/testing": "^10.4.22", + "@playwright/test": "^1.62.1", "@types/bcrypt": "^5.0.2", + "@types/cookie-parser": "^1.4.10", "@types/jest": "^29.5.14", - "@types/node": "^22.13.10", + "@types/node": "^22.20.1", "@types/passport-jwt": "^4.0.1", "@types/supertest": "^6.0.3", "@types/swagger-ui-express": "^4.1.8", "@typescript-eslint/eslint-plugin": "^8.25.0", "@typescript-eslint/parser": "^8.25.0", "eslint": "^9.21.0", - "@eslint/js": "^9.21.0", "globals": "^16.0.0", "jest": "^29.7.0", "prettier": "^3.5.3", @@ -65,6 +77,9 @@ "ts-jest": "^29.2.6", "ts-node": "^10.9.2", "ts-node-dev": "^2.0.0", - "typescript": "^5.8.2" + "typescript": "^5.9.3" + }, + "prisma": { + "seed": "node --loader ts-node/esm prisma/seed.ts" } } diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..5b753de --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,17 @@ +import { defineConfig, devices } from '@playwright/test'; + +export default defineConfig({ + // only run Playwright tests in the integration folder to avoid running Jest unit tests + testDir: './tests/integration', + timeout: 30 * 1000, + expect: { + timeout: 5000, + }, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + workers: process.env.CI ? 1 : undefined, + reporter: [["list"], ["./tests/reporter/custom-reporter.js"]], + use: { + baseURL: process.env.BASE_URL || 'http://localhost:3000', + }, +}); diff --git a/postman/RayLab.postman_collection.json b/postman/RayLab.postman_collection.json new file mode 100644 index 0000000..f94df2a --- /dev/null +++ b/postman/RayLab.postman_collection.json @@ -0,0 +1,58 @@ +{ + "info": { + "_postman_id": "e1e605cc-a2c6-4410-a7aa-4692b594d42d", + "name": "RayLab", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_exporter_id": "45313893" + }, + "item": [ + { + "name": "User", + "item": [ + { + "name": "Create User", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + }, + { + "key": "Authorization", + "value": "Bearer ", + "description": "Bearer ", + "type": "text", + "disabled": true + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"username\": \"rayyan\",\r\n \"email\": \"rayyan@example.com\",\r\n \"password\": \"Password123!\",\r\n \"fullName\": \"Rayyan\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:3000/api/v1/users", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "3000", + "path": [ + "api", + "v1", + "users" + ] + } + }, + "response": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/prisma/migrations/20260803160408_init/migration.sql b/prisma/migrations/20260803160408_init/migration.sql new file mode 100644 index 0000000..8d9d1ce --- /dev/null +++ b/prisma/migrations/20260803160408_init/migration.sql @@ -0,0 +1,277 @@ +-- CreateEnum +CREATE TYPE "UserRoleSource" AS ENUM ('AUTHENTIK', 'SYSTEM'); + +-- CreateTable +CREATE TABLE "User" ( + "id" UUID NOT NULL, + "authentikId" TEXT, + "authentikUserId" TEXT, + "authentikSubject" TEXT, + "username" VARCHAR(255), + "email" VARCHAR(320), + "name" VARCHAR(255), + "picture" TEXT, + "isActive" BOOLEAN NOT NULL DEFAULT true, + "deletedAt" TIMESTAMP(3), + "lastSeenAt" TIMESTAMP(3), + "lastSyncedAt" TIMESTAMP(3), + "syncStatus" TEXT, + "storageQuota" BIGINT DEFAULT 10737418240, + "storageUsed" BIGINT DEFAULT 0, + "metadata" JSONB, + "lastGroupHash" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "User_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Role" ( + "id" UUID NOT NULL, + "code" VARCHAR(100) NOT NULL, + "name" VARCHAR(255) NOT NULL, + "displayName" VARCHAR(255), + "description" TEXT, + "isDefault" BOOLEAN NOT NULL DEFAULT false, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Role_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Permission" ( + "id" UUID NOT NULL, + "code" VARCHAR(150) NOT NULL, + "name" VARCHAR(255) NOT NULL, + "displayName" VARCHAR(255), + "description" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Permission_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "RolePermission" ( + "id" UUID NOT NULL, + "roleId" UUID NOT NULL, + "permissionId" UUID NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "RolePermission_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "UserPermission" ( + "id" UUID NOT NULL, + "userId" UUID NOT NULL, + "permissionId" UUID NOT NULL, + "assignedBy" VARCHAR(100), + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "UserPermission_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "UserRole" ( + "id" UUID NOT NULL, + "userId" UUID NOT NULL, + "roleId" UUID NOT NULL, + "source" "UserRoleSource" NOT NULL DEFAULT 'AUTHENTIK', + "syncedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "UserRole_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "AuthGroupRoleMapping" ( + "id" UUID NOT NULL, + "authGroup" VARCHAR(255) NOT NULL, + "roleId" UUID NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "AuthGroupRoleMapping_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "AuditLog" ( + "id" UUID NOT NULL, + "userId" UUID, + "action" VARCHAR(200) NOT NULL, + "resource" VARCHAR(200), + "resourceId" TEXT, + "details" JSONB, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "AuditLog_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "ScheduledJob" ( + "id" UUID NOT NULL, + "name" VARCHAR(255) NOT NULL, + "payload" JSONB, + "cron" TEXT, + "runAt" TIMESTAMP(3), + "status" VARCHAR(50) NOT NULL DEFAULT 'pending', + "attempts" INTEGER NOT NULL DEFAULT 0, + "lastRunAt" TIMESTAMP(3), + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "ScheduledJob_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "MediaObject" ( + "id" UUID NOT NULL, + "ownerUserId" UUID, + "storageKey" VARCHAR(1024) NOT NULL, + "filename" VARCHAR(1024), + "mimeType" VARCHAR(255), + "size" BIGINT, + "isPublic" BOOLEAN NOT NULL DEFAULT false, + "metadata" JSONB, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "MediaObject_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Application" ( + "id" UUID NOT NULL DEFAULT gen_random_uuid(), + "code" VARCHAR(100) NOT NULL, + "name" VARCHAR(255) NOT NULL, + "description" TEXT, + "icon" TEXT, + "url" TEXT, + "applicationsClaim" VARCHAR(255) NOT NULL, + "isActive" BOOLEAN NOT NULL DEFAULT true, + "displayOrder" INTEGER NOT NULL DEFAULT 0, + "createdAt" TIMESTAMPTZ NOT NULL DEFAULT now(), + "updatedAt" TIMESTAMPTZ NOT NULL DEFAULT now(), + + CONSTRAINT "Application_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "User_authentikId_key" ON "User"("authentikId"); + +-- CreateIndex +CREATE UNIQUE INDEX "User_authentikUserId_key" ON "User"("authentikUserId"); + +-- CreateIndex +CREATE UNIQUE INDEX "User_authentikSubject_key" ON "User"("authentikSubject"); + +-- CreateIndex +CREATE UNIQUE INDEX "User_username_key" ON "User"("username"); + +-- CreateIndex +CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); + +-- CreateIndex +CREATE INDEX "User_authentikId_idx" ON "User"("authentikId"); + +-- CreateIndex +CREATE INDEX "User_username_idx" ON "User"("username"); + +-- CreateIndex +CREATE INDEX "User_email_idx" ON "User"("email"); + +-- CreateIndex +CREATE UNIQUE INDEX "Role_code_key" ON "Role"("code"); + +-- CreateIndex +CREATE UNIQUE INDEX "Permission_code_key" ON "Permission"("code"); + +-- CreateIndex +CREATE INDEX "RolePermission_roleId_idx" ON "RolePermission"("roleId"); + +-- CreateIndex +CREATE INDEX "RolePermission_permissionId_idx" ON "RolePermission"("permissionId"); + +-- CreateIndex +CREATE UNIQUE INDEX "RolePermission_roleId_permissionId_key" ON "RolePermission"("roleId", "permissionId"); + +-- CreateIndex +CREATE INDEX "UserPermission_userId_idx" ON "UserPermission"("userId"); + +-- CreateIndex +CREATE INDEX "UserPermission_permissionId_idx" ON "UserPermission"("permissionId"); + +-- CreateIndex +CREATE UNIQUE INDEX "UserPermission_userId_permissionId_key" ON "UserPermission"("userId", "permissionId"); + +-- CreateIndex +CREATE INDEX "UserRole_userId_idx" ON "UserRole"("userId"); + +-- CreateIndex +CREATE INDEX "UserRole_roleId_idx" ON "UserRole"("roleId"); + +-- CreateIndex +CREATE UNIQUE INDEX "UserRole_userId_roleId_key" ON "UserRole"("userId", "roleId"); + +-- CreateIndex +CREATE INDEX "AuthGroupRoleMapping_authGroup_idx" ON "AuthGroupRoleMapping"("authGroup"); + +-- CreateIndex +CREATE INDEX "AuditLog_userId_idx" ON "AuditLog"("userId"); + +-- CreateIndex +CREATE INDEX "AuditLog_action_idx" ON "AuditLog"("action"); + +-- CreateIndex +CREATE INDEX "ScheduledJob_name_idx" ON "ScheduledJob"("name"); + +-- CreateIndex +CREATE INDEX "ScheduledJob_status_idx" ON "ScheduledJob"("status"); + +-- CreateIndex +CREATE INDEX "MediaObject_ownerUserId_idx" ON "MediaObject"("ownerUserId"); + +-- CreateIndex +CREATE INDEX "MediaObject_storageKey_idx" ON "MediaObject"("storageKey"); + +-- CreateIndex +CREATE UNIQUE INDEX "Application_code_key" ON "Application"("code"); + +-- CreateIndex +CREATE UNIQUE INDEX "Application_applicationsClaim_key" ON "Application"("applicationsClaim"); + +-- CreateIndex +CREATE INDEX "Application_code_idx" ON "Application"("code"); + +-- CreateIndex +CREATE INDEX "Application_applicationsClaim_idx" ON "Application"("applicationsClaim"); + +-- AddForeignKey +ALTER TABLE "RolePermission" ADD CONSTRAINT "RolePermission_roleId_fkey" FOREIGN KEY ("roleId") REFERENCES "Role"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "RolePermission" ADD CONSTRAINT "RolePermission_permissionId_fkey" FOREIGN KEY ("permissionId") REFERENCES "Permission"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "UserPermission" ADD CONSTRAINT "UserPermission_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "UserPermission" ADD CONSTRAINT "UserPermission_permissionId_fkey" FOREIGN KEY ("permissionId") REFERENCES "Permission"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "UserRole" ADD CONSTRAINT "UserRole_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "UserRole" ADD CONSTRAINT "UserRole_roleId_fkey" FOREIGN KEY ("roleId") REFERENCES "Role"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "AuthGroupRoleMapping" ADD CONSTRAINT "AuthGroupRoleMapping_roleId_fkey" FOREIGN KEY ("roleId") REFERENCES "Role"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "AuditLog" ADD CONSTRAINT "AuditLog_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "MediaObject" ADD CONSTRAINT "MediaObject_ownerUserId_fkey" FOREIGN KEY ("ownerUserId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/prisma/migrations/20260902173944_add_debt_models/migration.sql b/prisma/migrations/20260902173944_add_debt_models/migration.sql new file mode 100644 index 0000000..ba2f820 --- /dev/null +++ b/prisma/migrations/20260902173944_add_debt_models/migration.sql @@ -0,0 +1,117 @@ +-- CreateEnum +CREATE TYPE "DebtTransactionType" AS ENUM ('DEBT', 'PAYMENT'); + +-- AlterTable +ALTER TABLE "Application" ALTER COLUMN "createdAt" SET DEFAULT now(), +ALTER COLUMN "updatedAt" SET DEFAULT now(); + +-- CreateTable +CREATE TABLE "DebtGroup" ( + "id" UUID NOT NULL, + "publicId" VARCHAR(20) NOT NULL, + "name" VARCHAR(255) NOT NULL, + "ownerId" UUID NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "DebtGroup_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "DebtGroupMember" ( + "id" UUID NOT NULL, + "groupId" UUID NOT NULL, + "userId" UUID NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "DebtGroupMember_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "DebtPerson" ( + "id" UUID NOT NULL, + "groupId" UUID NOT NULL, + "name" VARCHAR(255) NOT NULL, + "isDeleted" BOOLEAN NOT NULL DEFAULT false, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "DebtPerson_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "DebtTransaction" ( + "id" UUID NOT NULL, + "groupId" UUID NOT NULL, + "fromPersonId" UUID NOT NULL, + "toPersonId" UUID NOT NULL, + "amount" BIGINT NOT NULL, + "type" "DebtTransactionType" NOT NULL, + "description" TEXT, + "transactionDate" DATE NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "createdByUserId" UUID NOT NULL, + "requestId" VARCHAR(255), + + CONSTRAINT "DebtTransaction_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "DebtGroup_publicId_key" ON "DebtGroup"("publicId"); + +-- CreateIndex +CREATE INDEX "DebtGroup_publicId_idx" ON "DebtGroup"("publicId"); + +-- CreateIndex +CREATE INDEX "DebtGroup_ownerId_idx" ON "DebtGroup"("ownerId"); + +-- CreateIndex +CREATE INDEX "DebtGroupMember_groupId_idx" ON "DebtGroupMember"("groupId"); + +-- CreateIndex +CREATE INDEX "DebtGroupMember_userId_idx" ON "DebtGroupMember"("userId"); + +-- CreateIndex +CREATE UNIQUE INDEX "DebtGroupMember_groupId_userId_key" ON "DebtGroupMember"("groupId", "userId"); + +-- CreateIndex +CREATE INDEX "DebtPerson_groupId_idx" ON "DebtPerson"("groupId"); + +-- CreateIndex +CREATE UNIQUE INDEX "DebtPerson_groupId_name_key" ON "DebtPerson"("groupId", "name"); + +-- CreateIndex +CREATE INDEX "DebtTransaction_groupId_idx" ON "DebtTransaction"("groupId"); + +-- CreateIndex +CREATE INDEX "DebtTransaction_fromPersonId_idx" ON "DebtTransaction"("fromPersonId"); + +-- CreateIndex +CREATE INDEX "DebtTransaction_toPersonId_idx" ON "DebtTransaction"("toPersonId"); + +-- CreateIndex +CREATE UNIQUE INDEX "DebtTransaction_groupId_requestId_key" ON "DebtTransaction"("groupId", "requestId"); + +-- AddForeignKey +ALTER TABLE "DebtGroup" ADD CONSTRAINT "DebtGroup_ownerId_fkey" FOREIGN KEY ("ownerId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "DebtGroupMember" ADD CONSTRAINT "DebtGroupMember_groupId_fkey" FOREIGN KEY ("groupId") REFERENCES "DebtGroup"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "DebtGroupMember" ADD CONSTRAINT "DebtGroupMember_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "DebtPerson" ADD CONSTRAINT "DebtPerson_groupId_fkey" FOREIGN KEY ("groupId") REFERENCES "DebtGroup"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "DebtTransaction" ADD CONSTRAINT "DebtTransaction_groupId_fkey" FOREIGN KEY ("groupId") REFERENCES "DebtGroup"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "DebtTransaction" ADD CONSTRAINT "DebtTransaction_fromPersonId_fkey" FOREIGN KEY ("fromPersonId") REFERENCES "DebtPerson"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "DebtTransaction" ADD CONSTRAINT "DebtTransaction_toPersonId_fkey" FOREIGN KEY ("toPersonId") REFERENCES "DebtPerson"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "DebtTransaction" ADD CONSTRAINT "DebtTransaction_createdByUserId_fkey" FOREIGN KEY ("createdByUserId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml new file mode 100644 index 0000000..fbffa92 --- /dev/null +++ b/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/src/app.module.ts b/src/app.module.ts index 01cb319..e90b232 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -2,14 +2,49 @@ import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import { IdentityModule } from './modules/identity/identity.module'; +import { AuthModule } from './modules/auth/auth.module'; +import { HealthModule } from './modules/health/health.module'; +import { AuthorizationModule } from './modules/authorization/authorization.module'; +import { AuditModule } from './modules/audit/audit.module'; +import { ApplicationModule } from './modules/application/application.module'; +import { DebtModule } from './modules/bot-debt/debt.module'; + @Module({ imports: [ ConfigModule.forRoot({ - isGlobal: true, - }), - IdentityModule, + isGlobal: true, + // Load .env files depending on NODE_ENV. Default to development .env + envFilePath: process.env.NODE_ENV === 'production' ? '.env.production' : '.env', + // Basic validation: ensure expected frontend URLs and OIDC settings are present + validate: (env: Record) => { + const errors: string[] = []; + const nodeEnv = env.NODE_ENV || process.env.NODE_ENV || 'development'; + + if (!env.FRONTEND_URL) errors.push('FRONTEND_URL is not set'); + if (nodeEnv === 'production' && !env.PRODUCTION_FRONTEND_URL) errors.push('PRODUCTION_FRONTEND_URL is not set'); + + // OIDC required settings + if (!env.AUTHENTIK_ISSUER) errors.push('AUTHENTIK_ISSUER is not set'); + if (!env.AUTHENTIK_CLIENT_ID) errors.push('AUTHENTIK_CLIENT_ID is not set'); + if (!env.AUTHENTIK_CLIENT_SECRET) errors.push('AUTHENTIK_CLIENT_SECRET is not set'); + if (!env.AUTHENTIK_REDIRECT_URI) errors.push('AUTHENTIK_REDIRECT_URI is not set'); + + if (errors.length > 0) throw new Error('Environment validation error: ' + errors.join('; ')); + return env; + }, + }), + + IdentityModule, + AuthModule, + HealthModule, + // Authorization module provides permission checks and cache + AuthorizationModule, + AuditModule, + ApplicationModule, + DebtModule, + ], }) export class AppModule {} \ No newline at end of file diff --git a/src/common/constants/permission.constants.ts b/src/common/constants/permission.constants.ts new file mode 100644 index 0000000..29c7547 --- /dev/null +++ b/src/common/constants/permission.constants.ts @@ -0,0 +1,16 @@ +export const PermissionType = { + USER_CREATE: 'USER_CREATE', + USER_READ: 'USER_READ', + USER_UPDATE: 'USER_UPDATE', + USER_DELETE: 'USER_DELETE', + + ROLE_CREATE: 'ROLE_CREATE', + ROLE_READ: 'ROLE_READ', + ROLE_UPDATE: 'ROLE_UPDATE', + ROLE_DELETE: 'ROLE_DELETE', + + PERMISSION_CREATE: 'PERMISSION_CREATE', + PERMISSION_READ: 'PERMISSION_READ', + PERMISSION_UPDATE: 'PERMISSION_UPDATE', + PERMISSION_DELETE: 'PERMISSION_DELETE', +} as const; \ No newline at end of file diff --git a/src/core/auth/guards/jwt-auth.guard.ts b/src/core/auth/guards/jwt-auth.guard.ts index d29b625..15e7082 100644 --- a/src/core/auth/guards/jwt-auth.guard.ts +++ b/src/core/auth/guards/jwt-auth.guard.ts @@ -3,38 +3,102 @@ import { CanActivate, ExecutionContext, UnauthorizedException, + Logger, } from '@nestjs/common'; -import { JwtService } from '@nestjs/jwt'; import { Request } from 'express'; +import { createRemoteJWKSet, jwtVerify } from 'jose'; +import { IdentityData } from '../interfaces/identity-data'; +import * as jwt from 'jsonwebtoken'; +/** + * JwtAuthGuard verifies JWTs issued by the external Identity Provider (Authentik) + * using JWKS (RS256). It also accepts internal RayLab JWTs signed with a local secret. + */ @Injectable() export class JwtAuthGuard implements CanActivate { - constructor( - private readonly jwtService: JwtService, - ) {} + private jwks: ReturnType | null = null; + private readonly logger = new Logger('JwtAuthGuard'); + + constructor() {} async canActivate(context: ExecutionContext): Promise { - const request = context.switchToHttp().getRequest(); + const request = context.switchToHttp().getRequest(); + + let token: string | undefined; const authHeader = request.headers.authorization; - - if (!authHeader) { - throw new UnauthorizedException('Authorization header is missing.'); + if (authHeader) { + const [type, t] = authHeader.split(' '); + if (type === 'Bearer' && t) token = t; } - const [type, token] = authHeader.split(' '); + // fallback to cookie if no Authorization header + if (!token) { + token = (request as any).cookies?.raylab_jwt; + this.logger.debug(`No Authorization header. Trying cookie. cookiePresent=${!!(request as any).cookies} tokenFromCookie=${!!token}`); + } else { + this.logger.debug('Authorization header found. Using Bearer token.'); + } - if (type !== 'Bearer' || !token) { - throw new UnauthorizedException('Invalid authorization header.'); + if (!token) { + this.logger.debug('No token found in Authorization header or cookie.'); + throw new UnauthorizedException('Authorization token is missing.'); + } + + const jwksUri = process.env.AUTHENTIK_JWKS_URI; + + // First try verifying with external JWKS (Authentik) + if (jwksUri) { + try { + if (!this.jwks) this.jwks = createRemoteJWKSet(new URL(jwksUri)); + + const { payload } = await jwtVerify(token, this.jwks, { + issuer: process.env.AUTHENTIK_ISSUER, + audience: process.env.AUTHENTIK_AUDIENCE, + }); + + const identity = new IdentityData( + payload.sub as string, + (payload as any).preferred_username as string | undefined, + (payload as any).email as string | undefined, + payload as Record, + ); + + request.identity = identity; + // mark as external (verified by Authentik JWKS) + (request as any).identitySource = 'external'; + this.logger.debug(`Verified token using external JWKS. sub=${payload.sub}`); + return true; + } catch (err) { + this.logger.debug(`External JWKS verification failed: ${(err as Error).message}`); + // ignore and try internal verification + } + } + + // Fallback: verify with internal symmetric secret + const secret = process.env.RAYLAB_JWT_SECRET; + if (!secret) { + this.logger.error('RAYLAB_JWT_SECRET is not configured.'); + throw new UnauthorizedException('Invalid or expired token.'); } try { - const payload = await this.jwtService.verifyAsync(token); + const payload = jwt.verify(token, secret) as any; - request['user'] = payload; + const identity = new IdentityData( + payload.sub as string, + payload.preferred_username as string | undefined, + payload.email as string | undefined, + payload as Record, + ); + request.identity = identity; + // mark as internal (verified by RayLab internal secret) + (request as any).identitySource = 'internal'; + this.logger.debug(`Verified token using internal secret. sub=${payload.sub}`); return true; - } catch { + } catch (err: any) { + this.logger.debug(`Internal token verification failed: ${(err as Error).message}`); throw new UnauthorizedException('Invalid or expired token.'); } } diff --git a/src/core/auth/guards/jwt.guard.ts b/src/core/auth/guards/jwt.guard.ts new file mode 100644 index 0000000..f6e68bb --- /dev/null +++ b/src/core/auth/guards/jwt.guard.ts @@ -0,0 +1,22 @@ +import { Injectable, Logger, ExecutionContext } from '@nestjs/common'; +import { AuthGuard } from '@nestjs/passport'; + +@Injectable() +export class JwtGuard extends AuthGuard('jwt') { + private readonly logger = new Logger('JwtGuard'); + + canActivate(context: ExecutionContext) { + const req = context.switchToHttp().getRequest(); + this.logger.debug(`JwtGuard invoked. cookies=${JSON.stringify(req.cookies)} authHeader=${req.headers?.authorization}`); + return super.canActivate(context); + } + + handleRequest(err: any, user: any, info: any, context?: any) { + if (err || !user) { + this.logger.debug(`JwtGuard handleRequest failed. err=${err} user=${!!user} info=${JSON.stringify(info)}`); + } else { + this.logger.debug(`JwtGuard handleRequest success user=${JSON.stringify(user)}`); + } + return super.handleRequest(err, user, info, context); + } +} diff --git a/src/core/auth/guards/refresh.guard.ts b/src/core/auth/guards/refresh.guard.ts new file mode 100644 index 0000000..e407d2d --- /dev/null +++ b/src/core/auth/guards/refresh.guard.ts @@ -0,0 +1,22 @@ +import { Injectable, Logger, ExecutionContext } from '@nestjs/common'; +import { AuthGuard } from '@nestjs/passport'; + +@Injectable() +export class RefreshGuard extends AuthGuard('refresh') { + private readonly logger = new Logger('RefreshGuard'); + + canActivate(context: ExecutionContext) { + const req = context.switchToHttp().getRequest(); + this.logger.debug(`RefreshGuard invoked. cookies=${JSON.stringify(req.cookies)} body=${JSON.stringify(req.body)}`); + return super.canActivate(context); + } + + handleRequest(err: any, user: any, info: any, context?: any) { + if (err || !user) { + this.logger.debug(`RefreshGuard handleRequest failed. err=${err} user=${!!user} info=${JSON.stringify(info)}`); + } else { + this.logger.debug(`RefreshGuard handleRequest success userId=${user.id} authInfo=${JSON.stringify(info)}`); + } + return super.handleRequest(err, user, info, context); + } +} diff --git a/src/core/auth/interfaces/authenticated-request.interface.ts b/src/core/auth/interfaces/authenticated-request.interface.ts index 440f68b..4e9046d 100644 --- a/src/core/auth/interfaces/authenticated-request.interface.ts +++ b/src/core/auth/interfaces/authenticated-request.interface.ts @@ -1,12 +1,17 @@ import { Request } from 'express'; +import { UserData } from '../../../modules/identity/domain/entities/user.entity'; +import { IdentityData } from './identity-data'; -export interface JwtPayload { - sub: string; - email: string; - role: string; - permissions: string[]; -} +export type IdentitySource = 'internal' | 'external'; export interface AuthenticatedRequest extends Request { - user: JwtPayload; + // Identity comes from the external Identity Provider (Authentik) + // JwtAuthGuard must set request.identity = payload + identity?: IdentityData; + + // Indicate verification source: 'external' => verified via Authentik JWKS; 'internal' => verified via RayLab internal secret + identitySource?: IdentitySource; + + // After CurrentUserGuard resolves the user from repository, it must set request.currentUser = User Domain + currentUser?: UserData; } \ No newline at end of file diff --git a/src/core/auth/interfaces/identity-data.ts b/src/core/auth/interfaces/identity-data.ts new file mode 100644 index 0000000..640aca7 --- /dev/null +++ b/src/core/auth/interfaces/identity-data.ts @@ -0,0 +1,8 @@ +export class IdentityData { + constructor( + public readonly sub: string, + public readonly preferred_username?: string, + public readonly email?: string, + public readonly claims?: Record, + ) {} +} diff --git a/src/core/auth/strategies/jwt.strategy.ts b/src/core/auth/strategies/jwt.strategy.ts new file mode 100644 index 0000000..8ed4df9 --- /dev/null +++ b/src/core/auth/strategies/jwt.strategy.ts @@ -0,0 +1,29 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { PassportStrategy } from '@nestjs/passport'; +import { Strategy as JwtStrategyBase, ExtractJwt } from 'passport-jwt'; +import { ConfigService } from '@nestjs/config'; + +function cookieExtractor(req: any): string | null { + if (!req) return null; + if (req.cookies && req.cookies.raylab_jwt) return req.cookies.raylab_jwt as string; + return null; +} + +@Injectable() +export class JwtStrategy extends PassportStrategy(JwtStrategyBase, 'jwt') { + private readonly logger = new Logger('JwtStrategy'); + + constructor(private config: ConfigService) { + super({ + jwtFromRequest: ExtractJwt.fromExtractors([cookieExtractor, ExtractJwt.fromAuthHeaderAsBearerToken()]), + secretOrKey: config.get('RAYLAB_JWT_SECRET') || process.env.RAYLAB_JWT_SECRET || 'raylab-secret', + algorithms: ['HS256'], + }); + this.logger.debug('JwtStrategy initialized'); + } + + async validate(payload: any) { + this.logger.debug(`JwtStrategy.validate payload=${JSON.stringify(payload)}`); + return payload; // attached to req.user + } +} diff --git a/src/core/auth/strategies/refresh.strategy.ts b/src/core/auth/strategies/refresh.strategy.ts new file mode 100644 index 0000000..0d51b30 --- /dev/null +++ b/src/core/auth/strategies/refresh.strategy.ts @@ -0,0 +1,58 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { PassportStrategy } from '@nestjs/passport'; +import { Strategy } from 'passport-strategy'; +import { InMemoryRefreshStore } from '../../../modules/auth/refresh/inmemory-refresh.store'; +import { IUser } from '../../../modules/identity/domain/repositories/user.interface'; +import { Inject } from '@nestjs/common'; + +// Minimal Passport Strategy for refresh tokens (non-JWT random tokens) +class RefreshTokenStrategy extends Strategy { + name = 'refresh'; + authenticate(req: any) { + // This will be overridden in PassportStrategy wrapper + this.error(new Error('Not implemented')); + } +} + +@Injectable() +export class RefreshStrategy extends PassportStrategy(RefreshTokenStrategy, 'refresh') { + private readonly logger = new Logger('RefreshStrategy'); + + constructor(private refreshStore: InMemoryRefreshStore, @Inject(IUser) private userRepository: IUser) { + super(); + this.logger.debug('RefreshStrategy initialized'); + } + + async authenticate(req: any, options?: any) { + const cookies = req.cookies || null; + const token = (cookies && cookies.raylab_refresh) || (req.body && req.body.refreshToken); + this.logger.debug(`RefreshStrategy.authenticate cookies=${JSON.stringify(cookies)} tokenExtracted=${!!token}`); + + if (!token) { + this.logger.debug('RefreshStrategy: no refresh token provided'); + return this.fail('Missing refresh token', 401); + } + + try { + const data = await this.refreshStore.get(token); + if (!data) { + this.logger.debug('RefreshStrategy: refresh token not found/expired'); + return this.fail('Invalid refresh token', 401); + } + + const user = await this.userRepository.getById(data.userId); + if (!user) { + this.logger.debug('RefreshStrategy: user not found for refresh token'); + return this.fail('User not found', 401); + } + + // success - attach user + token info + const info = { refreshToken: token, userId: data.userId }; + this.logger.debug(`RefreshStrategy: validated refresh token for userId=${data.userId}`); + return this.success(user, info); + } catch (e) { + this.logger.debug(`RefreshStrategy error: ${(e as Error).message}`); + return this.error(e as Error); + } + } +} diff --git a/src/core/event-bus/event-bus.service.ts b/src/core/event-bus/event-bus.service.ts new file mode 100644 index 0000000..1b805a8 --- /dev/null +++ b/src/core/event-bus/event-bus.service.ts @@ -0,0 +1,36 @@ +import { Injectable, Logger } from '@nestjs/common'; + +import { EventEnvelope } from './event.interface'; + +type Handler = (event: EventEnvelope) => Promise | void; + +@Injectable() +export class EventBus { + private handlers: Map = new Map(); + private readonly logger = new Logger(EventBus.name); + + // Accept either an EventEnvelope or (type, payload) signature for backward compatibility + publish(eventOrType: EventEnvelope | string, payload?: any) { + let envelope: EventEnvelope; + if (typeof eventOrType === 'string') { + envelope = { id: require('crypto').randomUUID(), timestamp: new Date().toISOString(), type: eventOrType, payload: payload ?? {} }; + } else { + envelope = eventOrType; + } + + const handlers = this.handlers.get(envelope.type) || []; + for (const h of handlers) { + try { + Promise.resolve(h(envelope)).catch((err) => this.logger.error('Event handler error', err)); + } catch (e) { + this.logger.error('Event handler threw', e as any); + } + } + } + + subscribe(eventType: string, handler: Handler) { + const list = this.handlers.get(eventType) || []; + list.push(handler); + this.handlers.set(eventType, list); + } +} diff --git a/src/core/event-bus/event.interface.ts b/src/core/event-bus/event.interface.ts new file mode 100644 index 0000000..d1fb4ab --- /dev/null +++ b/src/core/event-bus/event.interface.ts @@ -0,0 +1,6 @@ +export interface EventEnvelope { + id: string; + timestamp: string; // ISO + type: string; // PascalCase event type + payload: T; +} diff --git a/src/main.ts b/src/main.ts index 6449bee..0255dd3 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,4 +1,5 @@ -import { ValidationPipe } from '@nestjs/common'; +import { ValidationPipe, Logger } from '@nestjs/common'; +import cookieParser from 'cookie-parser'; import { NestFactory } from '@nestjs/core'; import { ConfigService } from '@nestjs/config'; import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; @@ -9,8 +10,12 @@ async function bootstrap() { const app = await NestFactory.create(AppModule); const config = app.get(ConfigService); + const logger = new Logger('Bootstrap'); - app.setGlobalPrefix('api'); + // enable cookie parser so req.cookies is populated + app.use(cookieParser()); + + app.setGlobalPrefix(''); app.useGlobalPipes( new ValidationPipe({ @@ -20,10 +25,27 @@ async function bootstrap() { }), ); - app.enableCors(); + // CORS configuration: only allow configured frontend origins and enable credentials + const allowedOrigins: string[] = []; + const frontend = config.get('FRONTEND_URL'); + const prodFrontend = config.get('PRODUCTION_FRONTEND_URL'); + if (frontend) allowedOrigins.push(frontend); + if (prodFrontend) allowedOrigins.push(prodFrontend); - const swaggerEnabled = - config.get('SWAGGER_ENABLED') === 'true'; + app.enableCors({ + origin: allowedOrigins, + credentials: true, + methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'], + allowedHeaders: [ + 'Content-Type', + 'Authorization', + 'Accept', + 'Origin', + 'X-Requested-With', + ], + }); + + const swaggerEnabled = config.get('SWAGGER_ENABLED') === 'true'; if (swaggerEnabled) { const swaggerConfig = new DocumentBuilder() @@ -35,14 +57,17 @@ async function bootstrap() { const document = SwaggerModule.createDocument(app, swaggerConfig); - SwaggerModule.setup('docs', app, document); + SwaggerModule.setup('ApiList', app, document); } - const port = config.get('PORT') || 3000; + const port = config.get('PORT') || 3000; + + logger.log(`Allowed CORS origins: ${JSON.stringify(allowedOrigins)}`); + logger.log(`Server listening on port ${port}`); await app.listen(port); - console.log(`Server running on http://localhost:${port}`); + logger.log(`Server running on http://localhost:${port}`); } bootstrap(); \ No newline at end of file diff --git a/src/modules/application/application.module.ts b/src/modules/application/application.module.ts new file mode 100644 index 0000000..f54a3ed --- /dev/null +++ b/src/modules/application/application.module.ts @@ -0,0 +1,36 @@ +import { Module } from '@nestjs/common'; +import { PrismaService } from '../../shared/prisma.service'; +import { ApplicationsController } from './presentation/controllers/applications.controller'; +import { PrismaApplicationRepository } from './infrastructure/repositories/prisma-application.repository'; +import { IApplication } from './domain/repositories/application.interface'; +import { GetApplicationsHandler } from './application/handlers/get-applications.handler'; +import { GetApplicationHandler } from './application/handlers/get-application.handler'; +import { CreateApplicationHandler } from './application/handlers/create-application.handler'; +import { UpdateApplicationHandler } from './application/handlers/update-application.handler'; +import { DeleteApplicationHandler } from './application/handlers/delete-application.handler'; +import { GetMeApplicationsHandler } from './application/handlers/get-me-applications.handler'; +import { ApplicationValidator } from './application/validators/application.validator'; +import { JwtAuthGuard } from '../../core/auth/guards/jwt-auth.guard'; +import { IdentityModule } from '../identity/identity.module'; + +@Module({ + imports: [IdentityModule], + providers: [ + PrismaService, + PrismaApplicationRepository, + GetApplicationsHandler, + GetApplicationHandler, + CreateApplicationHandler, + UpdateApplicationHandler, + DeleteApplicationHandler, + GetMeApplicationsHandler, + ApplicationValidator, + JwtAuthGuard, + { + provide: IApplication, + useClass: PrismaApplicationRepository, + }, + ], + controllers: [ApplicationsController], +}) +export class ApplicationModule {} diff --git a/src/modules/application/application/handlers/create-application.handler.ts b/src/modules/application/application/handlers/create-application.handler.ts new file mode 100644 index 0000000..b04d1fd --- /dev/null +++ b/src/modules/application/application/handlers/create-application.handler.ts @@ -0,0 +1,26 @@ +import { Injectable, BadRequestException } from '@nestjs/common'; +import { IApplication } from '../../domain/repositories/application.interface'; +import { ApplicationValidator } from '../../application/validators/application.validator'; +import { ApplicationData } from '../../domain/entities/application.entity'; + +@Injectable() +export class CreateApplicationHandler { + constructor(private readonly appRepo: IApplication, private readonly validator: ApplicationValidator) {} + + async execute(payload: any) { + await this.validator.validateCreate(payload); + + const app = ApplicationData.create({ + code: payload.code, + name: payload.name, + description: payload.description, + icon: payload.icon, + url: payload.url, + applicationsClaim: payload.applicationsClaim, + displayOrder: payload.displayOrder, + }); + + const created = await this.appRepo.create(app); + return created; + } +} diff --git a/src/modules/application/application/handlers/delete-application.handler.ts b/src/modules/application/application/handlers/delete-application.handler.ts new file mode 100644 index 0000000..383353c --- /dev/null +++ b/src/modules/application/application/handlers/delete-application.handler.ts @@ -0,0 +1,13 @@ +import { Injectable } from '@nestjs/common'; +import { IApplication } from '../../domain/repositories/application.interface'; + +@Injectable() +export class DeleteApplicationHandler { + constructor(private readonly appRepo: IApplication) {} + + async execute(id: string) { + // ensure exists + await this.appRepo.findById(id); + await this.appRepo.delete(id); + } +} diff --git a/src/modules/application/application/handlers/get-application.handler.ts b/src/modules/application/application/handlers/get-application.handler.ts new file mode 100644 index 0000000..dc1eeb8 --- /dev/null +++ b/src/modules/application/application/handlers/get-application.handler.ts @@ -0,0 +1,12 @@ +import { Injectable } from '@nestjs/common'; +import { IApplication } from '../../domain/repositories/application.interface'; + +@Injectable() +export class GetApplicationHandler { + constructor(private readonly appRepo: IApplication) {} + + async execute(id: string) { + const app = await this.appRepo.findById(id); + return app; + } +} diff --git a/src/modules/application/application/handlers/get-applications.handler.ts b/src/modules/application/application/handlers/get-applications.handler.ts new file mode 100644 index 0000000..8af3a91 --- /dev/null +++ b/src/modules/application/application/handlers/get-applications.handler.ts @@ -0,0 +1,12 @@ +import { Injectable } from '@nestjs/common'; +import { IApplication } from '../../domain/repositories/application.interface'; + +@Injectable() +export class GetApplicationsHandler { + constructor(private readonly appRepo: IApplication) {} + + async execute(query: { page?: number; limit?: number; search?: string }) { + const res = await this.appRepo.find({ page: query.page, limit: query.limit, search: query.search || null }); + return res; + } +} diff --git a/src/modules/application/application/handlers/get-me-applications.handler.ts b/src/modules/application/application/handlers/get-me-applications.handler.ts new file mode 100644 index 0000000..c35274d --- /dev/null +++ b/src/modules/application/application/handlers/get-me-applications.handler.ts @@ -0,0 +1,17 @@ +import { Injectable } from '@nestjs/common'; +import { IApplication } from '../../domain/repositories/application.interface'; + +@Injectable() +export class GetMeApplicationsHandler { + constructor(private readonly appRepo: IApplication) {} + + async execute(applicationsClaimList: string[] | null | undefined) { + if (!applicationsClaimList || applicationsClaimList.length === 0) return { data: [], total: 0 }; + + // Only return active applications whose applicationsClaim exists in provided list + const res = await this.appRepo.find({ page: 1, limit: 1000, isActive: true, applicationsClaimIn: applicationsClaimList }); + // sort by displayOrder asc + res.data.sort((a, b) => (a.displayOrder ?? 0) - (b.displayOrder ?? 0)); + return res; + } +} diff --git a/src/modules/application/application/handlers/update-application.handler.ts b/src/modules/application/application/handlers/update-application.handler.ts new file mode 100644 index 0000000..e9b6fe1 --- /dev/null +++ b/src/modules/application/application/handlers/update-application.handler.ts @@ -0,0 +1,19 @@ +import { Injectable } from '@nestjs/common'; +import { IApplication } from '../../domain/repositories/application.interface'; +import { ApplicationValidator } from '../../application/validators/application.validator'; + +@Injectable() +export class UpdateApplicationHandler { + constructor(private readonly appRepo: IApplication, private readonly validator: ApplicationValidator) {} + + async execute(id: string, payload: any) { + const existing = await this.appRepo.findById(id); + if (!existing) throw new Error('Application not found'); + + await this.validator.validateUpdate(id, payload); + + existing.update(payload); + const updated = await this.appRepo.update(existing); + return updated; + } +} diff --git a/src/modules/application/application/validators/application.validator.ts b/src/modules/application/application/validators/application.validator.ts new file mode 100644 index 0000000..7d0833c --- /dev/null +++ b/src/modules/application/application/validators/application.validator.ts @@ -0,0 +1,64 @@ +import { Injectable, BadRequestException } from '@nestjs/common'; +import { IApplication } from '../../domain/repositories/application.interface'; + +@Injectable() +export class ApplicationValidator { + constructor(private readonly appRepo: IApplication) {} + + async validateCreate(payload: any) { + if (!payload || !payload.code) throw new BadRequestException('code is required'); + if (!payload.name) throw new BadRequestException('name is required'); + if (!payload.applicationsClaim) throw new BadRequestException('applicationsClaim is required'); + + // url validation if provided + if (payload.url) { + try { + // allow relative urls + if (!payload.url.startsWith('/') && !payload.url.startsWith('http')) { + throw new Error('invalid'); + } + // new URL(payload.url) // avoid throwing for relative + } catch (e) { + throw new BadRequestException('invalid url'); + } + } + + // displayOrder + if (payload.displayOrder !== undefined && typeof payload.displayOrder !== 'number') { + throw new BadRequestException('invalid displayOrder'); + } + + // unique code + const byCode = await this.appRepo.findByCode(payload.code); + if (byCode) throw new BadRequestException('duplicate code'); + + const byClaim = await this.appRepo.findByApplicationsClaim(payload.applicationsClaim); + if (byClaim) throw new BadRequestException('duplicate applicationsClaim'); + } + + async validateUpdate(id: string, payload: any) { + if (!payload) return; + + if (payload.url) { + try { + if (!payload.url.startsWith('/') && !payload.url.startsWith('http')) throw new Error('invalid'); + } catch (e) { + throw new BadRequestException('invalid url'); + } + } + + if (payload.displayOrder !== undefined && typeof payload.displayOrder !== 'number') { + throw new BadRequestException('invalid displayOrder'); + } + + if (payload.code) { + const existing = await this.appRepo.findByCode(payload.code); + if (existing && existing.id !== id) throw new BadRequestException('duplicate code'); + } + + if (payload.applicationsClaim) { + const existing = await this.appRepo.findByApplicationsClaim(payload.applicationsClaim); + if (existing && existing.id !== id) throw new BadRequestException('duplicate applicationsClaim'); + } + } +} diff --git a/src/modules/application/domain/entities/application.entity.ts b/src/modules/application/domain/entities/application.entity.ts new file mode 100644 index 0000000..0601738 --- /dev/null +++ b/src/modules/application/domain/entities/application.entity.ts @@ -0,0 +1,100 @@ +export class ApplicationData { + private constructor( + public readonly id: string, + public code: string, + public name: string, + public description: string | null, + public icon: string | null, + public url: string | null, + public applicationsClaim: string, + public isActive: boolean, + public displayOrder: number, + public createdAt: Date | null, + public updatedAt: Date | null, + ) {} + + static create(data: { + code: string; + name: string; + description?: string | null; + icon?: string | null; + url?: string | null; + applicationsClaim: string; + displayOrder?: number; + }) { + return new ApplicationData( + crypto.randomUUID(), + data.code, + data.name, + data.description ?? null, + data.icon ?? null, + data.url ?? null, + data.applicationsClaim, + true, + data.displayOrder ?? 0, + new Date(), + new Date(), + ); + } + + static restore(props: { + id: string; + code: string; + name: string; + description?: string | null; + icon?: string | null; + url?: string | null; + applicationsClaim: string; + isActive?: boolean; + displayOrder?: number; + createdAt?: Date | null; + updatedAt?: Date | null; + }) { + return new ApplicationData( + props.id, + props.code, + props.name, + props.description ?? null, + props.icon ?? null, + props.url ?? null, + props.applicationsClaim, + props.isActive !== undefined ? props.isActive : true, + props.displayOrder ?? 0, + props.createdAt ?? null, + props.updatedAt ?? null, + ); + } + + update(data: { + code?: string; + name?: string; + description?: string | null; + icon?: string | null; + url?: string | null; + applicationsClaim?: string; + isActive?: boolean; + displayOrder?: number; + }) { + if (data.code !== undefined) this.code = data.code; + if (data.name !== undefined) this.name = data.name; + if (data.description !== undefined) this.description = data.description; + if (data.icon !== undefined) this.icon = data.icon; + if (data.url !== undefined) this.url = data.url; + if (data.applicationsClaim !== undefined) this.applicationsClaim = data.applicationsClaim; + if (data.isActive !== undefined) this.isActive = data.isActive; + if (data.displayOrder !== undefined) this.displayOrder = data.displayOrder; + this.updatedAt = new Date(); + } + + toResponse() { + return { + id: this.id, + code: this.code, + name: this.name, + description: this.description, + icon: this.icon, + url: this.url, + displayOrder: this.displayOrder, + }; + } +} diff --git a/src/modules/application/domain/repositories/application.interface.ts b/src/modules/application/domain/repositories/application.interface.ts new file mode 100644 index 0000000..55e855b --- /dev/null +++ b/src/modules/application/domain/repositories/application.interface.ts @@ -0,0 +1,12 @@ +import { ApplicationData } from '../entities/application.entity'; + +export abstract class IApplication { + abstract find(params: { page?: number; limit?: number; search?: string | null; isActive?: boolean | null; applicationsClaimIn?: string[] | null }): Promise<{ data: ApplicationData[]; total: number }>; + abstract findById(id: string): Promise; + abstract findByCode(code: string): Promise; + abstract findByApplicationsClaim(claim: string): Promise; + + abstract create(app: ApplicationData): Promise; + abstract update(app: ApplicationData): Promise; + abstract delete(appId: string): Promise; +} diff --git a/src/modules/application/infrastructure/mappers/prisma-application.mapper.ts b/src/modules/application/infrastructure/mappers/prisma-application.mapper.ts new file mode 100644 index 0000000..5f9ab4c --- /dev/null +++ b/src/modules/application/infrastructure/mappers/prisma-application.mapper.ts @@ -0,0 +1,19 @@ +import { ApplicationData } from '../../domain/entities/application.entity'; + +export class PrismaApplicationMapper { + static toDomain(model: any): ApplicationData { + return ApplicationData.restore({ + id: model.id, + code: model.code, + name: model.name, + description: model.description, + icon: model.icon, + url: model.url, + applicationsClaim: model.applicationsClaim, + isActive: model.isActive, + displayOrder: model.displayOrder, + createdAt: model.createdAt, + updatedAt: model.updatedAt, + }); + } +} diff --git a/src/modules/application/infrastructure/repositories/prisma-application.repository.ts b/src/modules/application/infrastructure/repositories/prisma-application.repository.ts new file mode 100644 index 0000000..d7be1b1 --- /dev/null +++ b/src/modules/application/infrastructure/repositories/prisma-application.repository.ts @@ -0,0 +1,92 @@ +import { Injectable } from '@nestjs/common'; +import { PrismaService } from '../../../../shared/prisma.service'; +import { IApplication } from '../../domain/repositories/application.interface'; +import { PrismaApplicationMapper } from '../mappers/prisma-application.mapper'; +import { ApplicationData } from '../../domain/entities/application.entity'; + +@Injectable() +export class PrismaApplicationRepository implements IApplication { + constructor(private readonly prisma: PrismaService) {} + + async find(params: { page?: number; limit?: number; search?: string | null; isActive?: boolean | null; applicationsClaimIn?: string[] | null }) { + const page = params.page && params.page > 0 ? params.page : 1; + const limit = params.limit && params.limit > 0 ? params.limit : 25; + + const where: any = {}; + if (params.search) { + where.OR = [ + { name: { contains: params.search, mode: 'insensitive' } }, + { code: { contains: params.search, mode: 'insensitive' } }, + { description: { contains: params.search, mode: 'insensitive' } }, + ]; + } + + if (params.isActive !== undefined && params.isActive !== null) { + where.isActive = params.isActive; + } + + if (params.applicationsClaimIn && params.applicationsClaimIn.length > 0) { + where.applicationsClaim = { in: params.applicationsClaimIn }; + } + + const [total, items] = await Promise.all([ + this.prisma.application.count({ where }), + this.prisma.application.findMany({ where, skip: (page - 1) * limit, take: limit, orderBy: { displayOrder: 'asc', createdAt: 'asc' } }), + ]); + + return { data: items.map(i => PrismaApplicationMapper.toDomain(i)), total }; + } + + async findById(id: string) { + const row = await this.prisma.application.findUnique({ where: { id } }); + if (!row) throw new Error('Application not found'); + return PrismaApplicationMapper.toDomain(row); + } + + async findByCode(code: string) { + const row = await this.prisma.application.findUnique({ where: { code } }); + if (!row) return null; + return PrismaApplicationMapper.toDomain(row); + } + + async findByApplicationsClaim(claim: string) { + const row = await this.prisma.application.findUnique({ where: { applicationsClaim: claim } }); + if (!row) return null; + return PrismaApplicationMapper.toDomain(row); + } + + async create(app: ApplicationData) { + const created = await this.prisma.application.create({ data: { + id: app.id, + code: app.code, + name: app.name, + description: app.description, + icon: app.icon, + url: app.url, + applicationsClaim: app.applicationsClaim, + isActive: app.isActive, + displayOrder: app.displayOrder, + } }); + + return PrismaApplicationMapper.toDomain(created); + } + + async update(app: ApplicationData) { + const updated = await this.prisma.application.update({ where: { id: app.id }, data: { + code: app.code, + name: app.name, + description: app.description, + icon: app.icon, + url: app.url, + applicationsClaim: app.applicationsClaim, + isActive: app.isActive, + displayOrder: app.displayOrder, + } }); + + return PrismaApplicationMapper.toDomain(updated); + } + + async delete(appId: string) { + await this.prisma.application.delete({ where: { id: appId } }); + } +} diff --git a/src/modules/application/presentation/controllers/applications.controller.ts b/src/modules/application/presentation/controllers/applications.controller.ts new file mode 100644 index 0000000..2063864 --- /dev/null +++ b/src/modules/application/presentation/controllers/applications.controller.ts @@ -0,0 +1,76 @@ +import { Controller, Get, Param, UseGuards, Query, Patch, Delete, Post, Body, Req } from '@nestjs/common'; +import { ApiTags, ApiOperation } from '@nestjs/swagger'; +import { JwtAuthGuard } from '../../../../core/auth/guards/jwt-auth.guard'; +import { CurrentUserGuard } from '../../../identity/presentation/guards/current-user.guard'; +import { GetApplicationsHandler } from '../../application/handlers/get-applications.handler'; +import { GetApplicationHandler } from '../../application/handlers/get-application.handler'; +import { CreateApplicationHandler } from '../../application/handlers/create-application.handler'; +import { UpdateApplicationHandler } from '../../application/handlers/update-application.handler'; +import { DeleteApplicationHandler } from '../../application/handlers/delete-application.handler'; +import { GetMeApplicationsHandler } from '../../application/handlers/get-me-applications.handler'; + +@ApiTags('Applications') +@Controller() +export class ApplicationsController { + constructor( + private readonly getApplicationsHandler: GetApplicationsHandler, + private readonly getApplicationHandler: GetApplicationHandler, + private readonly createApplicationHandler: CreateApplicationHandler, + private readonly updateApplicationHandler: UpdateApplicationHandler, + private readonly deleteApplicationHandler: DeleteApplicationHandler, + private readonly getMeApplicationsHandler: GetMeApplicationsHandler, + ) {} + + @Get('applications') + @UseGuards(JwtAuthGuard, CurrentUserGuard) + @ApiOperation({ summary: 'List applications' }) + async findAll(@Query() query: any) { + const res = await this.getApplicationsHandler.execute({ page: query.page, limit: query.limit, search: query.search }); + return { success: true, data: res.data.map(a => a.toResponse ? a.toResponse() : a), meta: { total: res.total } }; + } + + @Get('applications/:id') + @UseGuards(JwtAuthGuard, CurrentUserGuard) + @ApiOperation({ summary: 'Get application by id' }) + async findOne(@Param('id') id: string) { + const app = await this.getApplicationHandler.execute(id); + return { success: true, data: app.toResponse(), meta: {} }; + } + + @Post('applications') + @UseGuards(JwtAuthGuard, CurrentUserGuard) + @ApiOperation({ summary: 'Create application' }) + async create(@Body() body: any) { + const created = await this.createApplicationHandler.execute(body); + return { success: true, data: created.toResponse(), meta: {} }; + } + + @Patch('applications/:id') + @UseGuards(JwtAuthGuard, CurrentUserGuard) + @ApiOperation({ summary: 'Update application' }) + async update(@Param('id') id: string, @Body() body: any) { + const updated = await this.updateApplicationHandler.execute(id, body); + return { success: true, data: updated.toResponse(), meta: {} }; + } + + @Delete('applications/:id') + @UseGuards(JwtAuthGuard, CurrentUserGuard) + @ApiOperation({ summary: 'Delete application' }) + async remove(@Param('id') id: string) { + await this.deleteApplicationHandler.execute(id); + return { success: true, data: null, meta: {} }; + } + + // Dashboard endpoint + @Get('me/applications') + @UseGuards(JwtAuthGuard, CurrentUserGuard) + @ApiOperation({ summary: "Get current user's applications (filtered by Authentik claims)" }) + async me(@Req() req: any) { + const ctx = req.raylabContext; + const identity = ctx && ctx.identity ? ctx.identity : {}; + const applicationsClaimList = identity.applications || []; + + const res = await this.getMeApplicationsHandler.execute(applicationsClaimList); + return { success: true, data: res.data.map(a => a.toResponse ? a.toResponse() : a), meta: { total: res.total } }; + } +} diff --git a/src/modules/audit/audit.event-handler.ts b/src/modules/audit/audit.event-handler.ts new file mode 100644 index 0000000..d4e1842 --- /dev/null +++ b/src/modules/audit/audit.event-handler.ts @@ -0,0 +1,46 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { EventBus } from '../../core/event-bus/event-bus.service'; +import { EventEnvelope } from '../../core/event-bus/event.interface'; +import { AuditService } from './audit.service'; + +@Injectable() +export class AuditEventHandler { + private readonly logger = new Logger(AuditEventHandler.name); + + constructor(private readonly events: EventBus, private readonly auditService: AuditService) { + this.events.subscribe('RolesSynchronized', (e) => this.handleRolesSynchronized(e)); + this.events.subscribe('UserAuthenticated', (e) => this.handleUserAuthenticated(e)); + this.events.subscribe('RoleCreated', (e) => this.handleGeneric(e)); + this.events.subscribe('RoleUpdated', (e) => this.handleGeneric(e)); + this.events.subscribe('RoleDeleted', (e) => this.handleGeneric(e)); + this.events.subscribe('PermissionCreated', (e) => this.handleGeneric(e)); + this.events.subscribe('PermissionUpdated', (e) => this.handleGeneric(e)); + this.events.subscribe('PermissionDeleted', (e) => this.handleGeneric(e)); + this.events.subscribe('UserActivated', (e) => this.handleGeneric(e)); + this.events.subscribe('UserDeactivated', (e) => this.handleGeneric(e)); + } + + async handleRolesSynchronized(event: EventEnvelope) { + try { + await this.auditService.createFromEvent(event); + } catch (e) { + this.logger.error('Audit handler failed', e as any); + } + } + + async handleUserAuthenticated(event: EventEnvelope) { + try { + await this.auditService.createFromEvent(event); + } catch (e) { + this.logger.error('Audit handler failed', e as any); + } + } + + async handleGeneric(event: EventEnvelope) { + try { + await this.auditService.createFromEvent(event); + } catch (e) { + this.logger.error('Audit handler failed', e as any); + } + } +} diff --git a/src/modules/audit/audit.module.ts b/src/modules/audit/audit.module.ts new file mode 100644 index 0000000..6f7b286 --- /dev/null +++ b/src/modules/audit/audit.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { AuditService } from './audit.service'; +import { AuditEventHandler } from './audit.event-handler'; +import { PrismaService } from '../../shared/prisma.service'; +import { EventBus } from '../../core/event-bus/event-bus.service'; + +@Module({ + providers: [AuditService, AuditEventHandler, PrismaService, EventBus], + exports: [AuditService], +}) +export class AuditModule {} diff --git a/src/modules/audit/audit.service.ts b/src/modules/audit/audit.service.ts new file mode 100644 index 0000000..5526b94 --- /dev/null +++ b/src/modules/audit/audit.service.ts @@ -0,0 +1,28 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { PrismaService } from '../../shared/prisma.service'; +import { EventEnvelope } from '../../core/event-bus/event.interface'; + +@Injectable() +export class AuditService { + private readonly logger = new Logger(AuditService.name); + + constructor(private readonly prisma: PrismaService) {} + + async createFromEvent(event: EventEnvelope) { + try { + // Map standard events to AuditLog entries + await this.prisma.auditLog.create({ + data: { + userId: event.payload?.userId || null, + action: event.type, + resource: event.payload?.resource || null, + resourceId: event.payload?.resourceId || null, + details: event.payload, + createdAt: new Date(event.timestamp), + }, + }); + } catch (e) { + this.logger.error('Failed to write audit log', e as any); + } + } +} diff --git a/src/modules/auth/auth.controller.ts b/src/modules/auth/auth.controller.ts new file mode 100644 index 0000000..42516e9 --- /dev/null +++ b/src/modules/auth/auth.controller.ts @@ -0,0 +1,101 @@ + +import { Controller, Get, Post, Query, Res, Req, Body, HttpCode, HttpStatus, Logger, UseGuards } from '@nestjs/common'; +import { ApiTags, ApiOperation } from '@nestjs/swagger'; +import { AuthService } from './auth.service'; +import { Response, Request } from 'express'; +import { JwtGuard } from '../../core/auth/guards/jwt.guard'; +import { RefreshGuard } from '../../core/auth/guards/refresh.guard'; +import { ConfigService } from '@nestjs/config'; + +@ApiTags('Auth') +@Controller('auth') +export class AuthController { + private readonly logger = new Logger('AuthController'); + constructor(private readonly authService: AuthService, private readonly config: ConfigService) {} + + @Get('login') + @ApiOperation({ summary: 'Start Authorization Code + PKCE login (redirect to Identity Provider)' }) + async login(@Query('returnTo') returnTo: string | undefined, @Res() res: Response) { + const redirect = await this.authService.createAuthorizationRedirect(returnTo); + return res.redirect(302, redirect); + } + + @Get('callback') + @ApiOperation({ summary: 'OIDC callback endpoint' }) + async callback(@Query('code') code: string, @Query('state') state: string, @Res() res: Response) { + const result = await this.authService.handleCallback(code, state); + + // set cookies with environment-aware options + const isProd = this.config.get('NODE_ENV') === 'production' || process.env.NODE_ENV === 'production'; + const cookieDomain = this.config.get('RAYLAB_COOKIE_DOMAIN') || (isProd ? '.raylab.site' : undefined); + + const cookieOptions: any = { + httpOnly: true, + secure: isProd, // secure in production + sameSite: isProd ? 'none' : 'lax', // cross-site in production + path: '/', + }; + if (cookieDomain) cookieOptions.domain = cookieDomain; + + // access token cookie (internal JWT) + this.logger.log(`Setting access & refresh cookies. cookieOptions=${JSON.stringify(cookieOptions)}`); + res.cookie('raylab_jwt', result.accessToken, { ...cookieOptions, maxAge: result.expiresIn * 1000 }); + + // refresh token cookie + res.cookie('raylab_refresh', result.refreshToken, { ...cookieOptions, maxAge: result.refreshTtl * 1000 }); + + this.logger.debug(`Callback complete. returnTo=${result.returnTo} user=${JSON.stringify(result.user)}`); + return res.redirect(302, result.returnTo || '/'); + } + + @Post('logout') + @HttpCode(HttpStatus.OK) + @ApiOperation({ summary: 'Logout (invalidate internal session and redirect to identity provider logout)' }) + async logout(@Req() req: Request, @Res() res: Response) { + const refreshToken = req.cookies?.raylab_refresh || req.body?.refreshToken; + const redirect = await this.authService.logout(refreshToken); + return res.redirect(302, redirect); + } + + @Post('refresh') + @UseGuards(RefreshGuard) + @ApiOperation({ summary: 'Refresh internal JWT using internal refresh token' }) + async refresh(@Req() req: Request, @Res() res: Response) { + // req.authInfo should contain { refreshToken } + const refreshToken = (req as any).authInfo?.refreshToken || (req.cookies?.raylab_refresh) || (req.body?.refreshToken); + this.logger.debug(`Refresh called (guarded). cookies=${JSON.stringify((req as any).cookies)} authInfo=${JSON.stringify((req as any).authInfo)}`); + + const result = await this.authService.refresh(refreshToken); + + // set cookies with environment-aware options + const isProd = this.config.get('NODE_ENV') === 'production' || process.env.NODE_ENV === 'production'; + const cookieDomain = this.config.get('RAYLAB_COOKIE_DOMAIN') || (isProd ? '.raylab.site' : undefined); + + const cookieOptions: any = { + httpOnly: true, + secure: isProd, + sameSite: isProd ? 'none' : 'lax', + path: '/', + }; + if (cookieDomain) cookieOptions.domain = cookieDomain; + + // set new cookies (rotate) + res.cookie('raylab_jwt', result.accessToken, { ...cookieOptions, maxAge: result.expiresIn * 1000 }); + res.cookie('raylab_refresh', result.refreshToken, { ...cookieOptions, maxAge: result.refreshTtl * 1000 }); + + this.logger.debug(`Refresh complete. set new cookies for user`); + return res.json({ success: true, data: result }); + } + + @Get('me') + @UseGuards(JwtGuard) + @ApiOperation({ summary: 'Get current user from internal JWT (cookie or Authorization header)' }) + async me(@Req() req: Request) { + // With JwtGuard, req.user should be payload from token + this.logger.debug(`Me called (guarded). cookies=${JSON.stringify((req as any).cookies)} user=${JSON.stringify((req as any).user)}`); + const payload = (req as any).user; + const user = await this.authService.me((req.cookies?.raylab_jwt) || (req.headers.authorization && (req.headers.authorization as string).replace(/^Bearer\s+/i, ''))); + this.logger.debug(`Me returning user id=${user?.id}`); + return { success: true, data: user }; + } +} diff --git a/src/modules/auth/auth.module.ts b/src/modules/auth/auth.module.ts new file mode 100644 index 0000000..5f16b6f --- /dev/null +++ b/src/modules/auth/auth.module.ts @@ -0,0 +1,77 @@ +import { Module } from '@nestjs/common'; +import { PassportModule } from '@nestjs/passport'; +import { JwtStrategy } from '../../core/auth/strategies/jwt.strategy'; +import { RefreshStrategy } from '../../core/auth/strategies/refresh.strategy'; +import { JwtGuard } from '../../core/auth/guards/jwt.guard'; +import { RefreshGuard } from '../../core/auth/guards/refresh.guard'; +import { AuthController } from './auth.controller'; +import { AuthService } from './auth.service'; +import { ConfigModule, ConfigService } from '@nestjs/config'; +import { JwtModule } from '@nestjs/jwt'; +import { PrismaService } from '../../shared/prisma.service'; +import { IUser } from '../identity/domain/repositories/user.interface'; +import { PrismaUserRepository } from '../identity/infrastructure/repositories/prisma-user.repository'; +import { SyncIdentityHandler } from '../identity/application/handlers/user/sync-identity.handler'; +import { IRole } from '../identity/domain/repositories/role.interface'; +import { PrismaRoleRepository } from '../identity/infrastructure/repositories/prisma-role.repository'; +import { IAuthConfig } from '../identity/application/config/i-auth-config'; +import { EnvAuthConfig } from '../identity/application/config/env-auth-config'; +import { RedisPkceStore } from './pkce/redis-pkce.store'; +import { InMemoryRefreshStore } from './refresh/inmemory-refresh.store'; +import { OidcService } from './oidc.service'; +import { RoleSyncService } from './role-sync.service'; +import { RedisService } from '../../shared/redis.service'; +import { GroupHashService } from './group-hash.service'; +import { AuthenticationService } from './authentication.service'; +import { EventBus } from '../../core/event-bus/event-bus.service'; +import { AuditService } from '../audit/audit.service'; +import { AuthorizationModule } from '../authorization/authorization.module'; + + +@Module({ + imports: [ + ConfigModule, + AuthorizationModule, + PassportModule.register({ defaultStrategy: 'jwt', session: false }), + JwtModule.registerAsync({ + imports: [ConfigModule], + useFactory: async (config: ConfigService) => ({ + secret: config.get('RAYLAB_JWT_SECRET') || 'raylab-secret', + signOptions: { algorithm: 'HS256' }, + }), + inject: [ConfigService], + }), + ], + controllers: [AuthController], + providers: [ + AuthService, + PrismaService, + PrismaUserRepository, + PrismaRoleRepository, + SyncIdentityHandler, + { provide: IUser, useClass: PrismaUserRepository }, + { provide: IRole, useClass: PrismaRoleRepository }, + { provide: IAuthConfig, useClass: EnvAuthConfig }, + + // In-memory PKCE and Refresh stores (Redis removed) + RedisPkceStore, + InMemoryRefreshStore, + + // OIDC & Role Sync + OidcService, + RoleSyncService, + GroupHashService, + AuthenticationService, + EventBus, + AuditService, + RedisService, + + // Strategies & Guards + JwtStrategy, + RefreshStrategy, + JwtGuard, + RefreshGuard, + ], + exports: [AuthService, OidcService, RoleSyncService, AuthenticationService, EventBus], +}) +export class AuthModule {} diff --git a/src/modules/auth/auth.service.ts b/src/modules/auth/auth.service.ts new file mode 100644 index 0000000..05d95dc --- /dev/null +++ b/src/modules/auth/auth.service.ts @@ -0,0 +1,236 @@ +import { Injectable, UnauthorizedException, Inject, Logger } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { JwtService } from '@nestjs/jwt'; +import { SyncIdentityHandler } from '../identity/application/handlers/user/sync-identity.handler'; +import { IUser } from '../identity/domain/repositories/user.interface'; +import { PrismaService } from '../../shared/prisma.service'; +import { RedisPkceStore } from './pkce/redis-pkce.store'; +import { InMemoryRefreshStore } from './refresh/inmemory-refresh.store'; +// dynamic require to avoid TypeScript typing issues with installed openid-client +// eslint-disable-next-line @typescript-eslint/no-var-requires +const OpenIDClient = require('openid-client'); +import crypto from 'crypto'; + +const logger = new Logger('AuthService'); + +@Injectable() +export class AuthService { + constructor( + private readonly config: ConfigService, + private readonly jwtService: JwtService, + private readonly prisma: PrismaService, + @Inject(IUser) private readonly userRepository: IUser, + private readonly syncIdentityHandler: SyncIdentityHandler, + private readonly pkceStore: RedisPkceStore, + private readonly refreshStore: InMemoryRefreshStore, + ) {} + + private issuer: any | null = null; + private client: any | null = null; + + private async getIssuer() { + if (this.issuer) return this.issuer; + const issuerUrl = this.config.get('AUTHENTIK_ISSUER'); + if (!issuerUrl) throw new Error('AUTHENTIK_ISSUER not configured'); + this.issuer = await OpenIDClient.Issuer.discover(issuerUrl); + return this.issuer; + } + + private async getClient() { + if (this.client) return this.client; + const issuer = await this.getIssuer(); + const clientId = this.config.get('AUTHENTIK_CLIENT_ID'); + const clientSecret = this.config.get('AUTHENTIK_CLIENT_SECRET'); + if (!clientId) throw new Error('AUTHENTIK_CLIENT_ID not configured'); + this.client = new issuer.Client({ client_id: clientId, client_secret: clientSecret }); + return this.client; + } + + async createAuthorizationRedirect(returnTo?: string): Promise { + const client = await this.getClient(); + const redirectUri = this.config.get('AUTHENTIK_REDIRECT_URI'); + + if (!redirectUri) { + throw new Error('AUTHENTIK_REDIRECT_URI is not configured'); + } + + const state = crypto.randomUUID(); + const code_verifier = OpenIDClient.generators.codeVerifier(); + const code_challenge = await OpenIDClient.generators.codeChallenge(code_verifier); + const nonce = OpenIDClient.generators.nonce(); + + // save PKCE session keyed by state + await this.pkceStore.save(state, { code_verifier, nonce, returnTo }, 300); + + const url = client.authorizationUrl({ + redirect_uri: redirectUri, + scope: this.config.get('AUTHENTIK_DEFAULT_SCOPE') || 'openid email profile', + response_type: 'code', + code_challenge, + code_challenge_method: 'S256', + state, + nonce, + }); + + return url; + } + + async handleCallback(code: string, state: string) { + const client = await this.getClient(); + + // retrieve PKCE session using state provided by the IdP + const pkce = await this.pkceStore.get(state); + if (!pkce) throw new UnauthorizedException('Invalid or expired state'); + + const redirectUri = this.config.get('AUTHENTIK_REDIRECT_URI'); + if (!redirectUri) throw new Error('AUTHENTIK_REDIRECT_URI is not configured'); + + // Exchange code for tokens. Provide explicit checks: state, nonce and code_verifier. + let tokenSet: any; + try { + tokenSet = await client.callback( + redirectUri, + { code, state }, + { state, nonce: pkce.nonce, code_verifier: pkce.code_verifier }, + ); + } catch (err) { + logger.debug('Authorization code exchange failed: ' + (err as Error).message); + // remove PKCE entry to avoid replay + await this.pkceStore.remove(state); + throw new UnauthorizedException('Authorization code exchange failed'); + } + + // remove PKCE entry after successful exchange + await this.pkceStore.remove(state); + + // verify id_token and get claims + const claims = tokenSet.claims(); + + // fetch userinfo if available + let userInfo: Record | null = null; + try { + if ((tokenSet as any).access_token && typeof client.userinfo === 'function') { + userInfo = await client.userinfo((tokenSet as any).access_token); + } + } catch (e) { + // ignore userinfo errors + } + + const identity = { + sub: (userInfo && (userInfo as any).sub) || (claims as any).sub || null, + preferred_username: + (userInfo && ((userInfo as any).preferred_username || (userInfo as any).username || (userInfo as any).email)) || + (claims as any).preferred_username || (claims as any).email, + email: (userInfo && (userInfo as any).email) || (claims as any).email, + raw: { tokenSet, userInfo, claims }, + } as any; + + // Sync identity to local user (create if needed) + const domainUser = await this.syncIdentityHandler.execute(identity as any); + + // ensure active/not deleted + if (!domainUser.isActive) throw new UnauthorizedException('User is not active'); + if (domainUser.deletedAt) throw new UnauthorizedException('User is deleted'); + + // create internal JWT + const jwtPayload = { + sub: domainUser.id, + preferred_username: domainUser.username, + email: domainUser.email, + }; + + const expiresIn = Number(this.config.get('RAYLAB_JWT_EXPIRES_IN') || 3600); + const access = this.jwtService.sign(jwtPayload, { expiresIn }); + + logger.debug(`Created internal access token for user=${domainUser.id} expiresIn=${expiresIn}`); + + // create internal refresh token + const refreshToken = crypto.randomUUID(); + const refreshTtl = Number(this.config.get('RAYLAB_REFRESH_EXPIRES_IN') || 30 * 24 * 3600); // default 30 days + + await this.refreshStore.set(refreshToken, { userId: domainUser.id }, refreshTtl); + logger.debug(`Stored refresh token for user=${domainUser.id} ttl=${refreshTtl}`); + + return { + accessToken: access, + refreshToken, + expiresIn, + refreshTtl, + user: { + id: domainUser.id, + username: domainUser.username, + email: domainUser.email, + roles: domainUser.roles || [], + }, + returnTo: pkce.returnTo, + }; + } + + async refresh(refreshToken?: string) { + if (!refreshToken) { + logger.debug('Refresh called without refresh token'); + throw new UnauthorizedException('Missing refresh token'); + } + const data = await this.refreshStore.get(refreshToken); + if (!data) { + logger.debug(`Refresh token not found or expired: ${refreshToken}`); + throw new UnauthorizedException('Invalid refresh token'); + } + logger.debug(`Refresh token validated for userId=${data.userId}`); + + const userId = data.userId; + // load user + const domainUser = await this.userRepository.getById(userId); + if (!domainUser) throw new UnauthorizedException('User not found'); + if (!domainUser.isActive) throw new UnauthorizedException('User is not active'); + + // rotate refresh token + await this.refreshStore.del(refreshToken); + const newRefresh = crypto.randomUUID(); + const refreshTtl = Number(this.config.get('RAYLAB_REFRESH_EXPIRES_IN') || 30 * 24 * 3600); + await this.refreshStore.set(newRefresh, { userId }, refreshTtl); + logger.debug(`Rotated refresh token for userId=${userId} newRefresh=${newRefresh} ttl=${refreshTtl}`); + + const expiresIn = Number(this.config.get('RAYLAB_JWT_EXPIRES_IN') || 3600); + const access = this.jwtService.sign({ sub: domainUser.id, preferred_username: domainUser.username, email: domainUser.email }, { expiresIn }); + logger.debug(`Issued new access token for user=${userId} expiresIn=${expiresIn}`); + + return { accessToken: access, refreshToken: newRefresh, expiresIn, refreshTtl }; + } + + async logout(refreshToken?: string) { + if (refreshToken) { + await this.refreshStore.del(refreshToken); + } + + const issuer = await this.getIssuer(); + const endSession = issuer.metadata.end_session_endpoint; + const postLogout = this.config.get('AUTHENTIK_POST_LOGOUT_REDIRECT') || '/'; + + if (endSession) { + // Redirect to identity provider logout + const url = new URL(endSession); + if (postLogout) url.searchParams.set('post_logout_redirect_uri', postLogout); + return url.toString(); + } + + return postLogout; + } + + async me(token?: string) { + if (!token) { + logger.debug('Me called without token'); + throw new UnauthorizedException('Missing token'); + } + try { + const payload: any = this.jwtService.verify(token); + logger.debug(`Token verified successfully. payload.sub=${payload.sub}`); + const user = await this.userRepository.getById(payload.sub); + if (!user) throw new UnauthorizedException('User not found'); + return { id: user.id, username: user.username, email: user.email, roles: user.roles || [] }; + } catch (e) { + logger.debug(`Token verification failed: ${(e as Error).message}`); + throw new UnauthorizedException('Invalid token'); + } + } +} diff --git a/src/modules/auth/authentication.service.ts b/src/modules/auth/authentication.service.ts new file mode 100644 index 0000000..9817ba6 --- /dev/null +++ b/src/modules/auth/authentication.service.ts @@ -0,0 +1,55 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { OidcService } from './oidc.service'; +import { PrismaService } from '../../shared/prisma.service'; +import { RoleSyncService } from './role-sync.service'; +import { AuthorizationService } from '../authorization/authorization.service'; +import { EventBus } from '../../core/event-bus/event-bus.service'; +import { RequestContext } from '../../shared/types/request-context'; + +@Injectable() +export class AuthenticationService { + private readonly logger = new Logger(AuthenticationService.name); + + constructor( + private readonly oidc: OidcService, + private readonly prisma: PrismaService, + private readonly roleSync: RoleSyncService, + private readonly authorization: AuthorizationService, + private readonly events: EventBus, + ) {} + + async authenticate(bearerToken: string): Promise { + // Validate token (signature/iss/aud/exp) + const claims = await this.oidc.verifyToken(bearerToken); + + const sub = claims.sub; + if (!sub) throw new Error('Invalid token: missing sub'); + + // Resolve identity + const identity = { sub, email: claims.email, preferred_username: claims.preferred_username, raw: claims }; + + // Find or create user (materialize) + let user = await (this.prisma as any).user.findUnique({ where: { authentikId: sub } }); + if (!user) { + user = await (this.prisma as any).user.create({ data: { authentikId: sub, username: identity.preferred_username || identity.email || sub, email: identity.email || null } }); + } + + // Synchronize roles + const groups: string[] = Array.isArray(claims.groups) ? claims.groups : []; + await this.roleSync.syncUserRolesFromAuthentik(user.id, groups); + + // Load permissions + const permissions = await this.authorization.getUserPermissions(user.id); + + // Build request context + const rolesRows = await (this.prisma as any).userRole.findMany({ where: { userId: user.id } }); + const roles = rolesRows.map((r: any) => r.roleId); + + const ctx: RequestContext = { user, identity, roles, permissions }; + + // Publish domain event (legacy string event name expected by some subscribers/tests) + this.events.publish('user.authenticated', { userId: user.id, identity }); + + return ctx; + } +} diff --git a/src/modules/auth/dto/login.dto.ts b/src/modules/auth/dto/login.dto.ts new file mode 100644 index 0000000..d3c2d25 --- /dev/null +++ b/src/modules/auth/dto/login.dto.ts @@ -0,0 +1,2 @@ +// Login DTO removed. Password grant has been removed in favor of Authorization Code + PKCE flow. +// Formerly contained username/password properties. diff --git a/src/modules/auth/group-hash.service.ts b/src/modules/auth/group-hash.service.ts new file mode 100644 index 0000000..32eb60d --- /dev/null +++ b/src/modules/auth/group-hash.service.ts @@ -0,0 +1,11 @@ +import { Injectable } from '@nestjs/common'; +import * as crypto from 'crypto'; + +@Injectable() +export class GroupHashService { + compute(groups: string[]): string { + const sorted = (groups || []).slice().sort(); + const data = sorted.join(','); + return crypto.createHash('sha256').update(data, 'utf8').digest('hex'); + } +} diff --git a/src/modules/auth/guards/oidc.guard.ts b/src/modules/auth/guards/oidc.guard.ts new file mode 100644 index 0000000..1bd0d27 --- /dev/null +++ b/src/modules/auth/guards/oidc.guard.ts @@ -0,0 +1,26 @@ +import { CanActivate, ExecutionContext, Injectable, UnauthorizedException, Logger, Inject } from '@nestjs/common'; +import { OidcService } from '../oidc.service'; +import { AuthenticationService } from '../authentication.service'; + +@Injectable() +export class OidcGuard implements CanActivate { + private readonly logger = new Logger(OidcGuard.name); + constructor(private readonly oidc: OidcService, private readonly authn: AuthenticationService) {} + + async canActivate(context: ExecutionContext): Promise { + const req = context.switchToHttp().getRequest(); + const auth = req.headers['authorization'] || req.headers['Authorization']; + if (!auth || typeof auth !== 'string' || !auth.startsWith('Bearer ')) throw new UnauthorizedException('Missing bearer token'); + const token = auth.substring(7).trim(); + + try { + const ctx = await this.authn.authenticate(token); + // attach context to request under a structured key + req.raylabContext = ctx; + return true; + } catch (e) { + this.logger.debug('Authentication failed', (e as any).message); + throw new UnauthorizedException('Invalid token or authentication failed'); + } + } +} diff --git a/src/modules/auth/oidc.service.ts b/src/modules/auth/oidc.service.ts new file mode 100644 index 0000000..42205b7 --- /dev/null +++ b/src/modules/auth/oidc.service.ts @@ -0,0 +1,37 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; + +@Injectable() +export class OidcService { + private jwksUri: string | null = null; + private issuer: string; + private audience: string | string[] | undefined; + private logger = new Logger(OidcService.name); + + constructor(private readonly config: ConfigService) { + this.issuer = this.config.get('AUTHENTIK_ISSUER') || ''; + this.audience = this.config.get('AUTHENTIK_AUDIENCE') || undefined; + const jwksUri = this.config.get('AUTHENTIK_JWKS_URI'); + if (jwksUri) this.jwksUri = jwksUri; + else if (this.issuer) this.jwksUri = `${this.issuer.replace(/\/+$/, '')}/.well-known/jwks.json`; + } + + async verifyToken(token: string) { + if (!this.jwksUri) throw new Error('JWKS not configured'); + + try { + // dynamic import to avoid ESM loading issues in test environment + const jose = await import('jose'); + const jwks = jose.createRemoteJWKSet(new URL(this.jwksUri)); + const { payload } = await jose.jwtVerify(token, jwks, { + issuer: this.issuer || undefined, + audience: this.audience, + } as any); + + return payload as Record; + } catch (e) { + this.logger.debug('Token verification failed', (e as Error).message); + throw e; + } + } +} diff --git a/src/modules/auth/pkce/redis-pkce.store.ts b/src/modules/auth/pkce/redis-pkce.store.ts new file mode 100644 index 0000000..2124c59 --- /dev/null +++ b/src/modules/auth/pkce/redis-pkce.store.ts @@ -0,0 +1,42 @@ +import { Injectable, OnModuleDestroy } from '@nestjs/common'; + +type PkceEntry = { code_verifier: string; nonce: string; returnTo?: string; expiresAt: number }; + +@Injectable() +export class RedisPkceStore implements OnModuleDestroy { + // In-memory PKCE store replacing Redis-backed implementation + private map = new Map(); + private cleanupInterval?: NodeJS.Timeout; + + constructor() { + // periodic cleanup + this.cleanupInterval = setInterval(() => { + const now = Date.now(); + for (const [k, v] of this.map.entries()) { + if (v.expiresAt <= now) this.map.delete(k); + } + }, 60 * 1000); + } + + private key(state: string) { return state; } + + async save(state: string, data: { code_verifier: string; nonce: string; returnTo?: string }, ttlSeconds = 300) { + const expiresAt = Date.now() + ttlSeconds * 1000; + this.map.set(this.key(state), { ...data, expiresAt }); + } + + async get(state: string) { + const v = this.map.get(this.key(state)); + if (!v) return null; + if (v.expiresAt <= Date.now()) { this.map.delete(this.key(state)); return null; } + return { code_verifier: v.code_verifier, nonce: v.nonce, returnTo: v.returnTo }; + } + + async remove(state: string) { + this.map.delete(this.key(state)); + } + + onModuleDestroy() { + if (this.cleanupInterval) clearInterval(this.cleanupInterval); + } +} diff --git a/src/modules/auth/refresh/inmemory-refresh.store.ts b/src/modules/auth/refresh/inmemory-refresh.store.ts new file mode 100644 index 0000000..5917283 --- /dev/null +++ b/src/modules/auth/refresh/inmemory-refresh.store.ts @@ -0,0 +1,38 @@ +import { Injectable, OnModuleDestroy } from '@nestjs/common'; + +type RefreshEntry = { userId: string; expiresAt: number }; + +@Injectable() +export class InMemoryRefreshStore implements OnModuleDestroy { + private map = new Map(); + private cleanupInterval?: NodeJS.Timeout; + + constructor() { + this.cleanupInterval = setInterval(() => { + const now = Date.now(); + for (const [k, v] of this.map.entries()) { + if (v.expiresAt <= now) this.map.delete(k); + } + }, 60 * 1000); + } + + async set(token: string, data: { userId: string }, ttlSeconds: number) { + const expiresAt = Date.now() + ttlSeconds * 1000; + this.map.set(token, { userId: data.userId, expiresAt }); + } + + async get(token: string) { + const v = this.map.get(token); + if (!v) return null; + if (v.expiresAt <= Date.now()) { this.map.delete(token); return null; } + return { userId: v.userId }; + } + + async del(token: string) { + this.map.delete(token); + } + + onModuleDestroy() { + if (this.cleanupInterval) clearInterval(this.cleanupInterval); + } +} diff --git a/src/modules/auth/role-sync.service.ts b/src/modules/auth/role-sync.service.ts new file mode 100644 index 0000000..2abc796 --- /dev/null +++ b/src/modules/auth/role-sync.service.ts @@ -0,0 +1,74 @@ +import { Injectable, Logger, Inject } from '@nestjs/common'; +import { PrismaService } from '../../shared/prisma.service'; +import { PermissionCache } from '../authorization/cache/permission-cache.interface'; +import { EventBus } from '../../core/event-bus/event-bus.service'; +import { GroupHashService } from './group-hash.service'; +import { PERMISSION_CACHE } from '../authorization/authorization.service'; + +@Injectable() +export class RoleSyncService { + private readonly logger = new Logger(RoleSyncService.name); + + constructor( + private readonly prisma: PrismaService, + private readonly groupHash: GroupHashService, + @Inject(PERMISSION_CACHE) private readonly permissionCache: PermissionCache, + private readonly events: EventBus, + ) {} + + computeGroupHash(groups: string[]): string { + return this.groupHash.compute(groups || []); + } + + async mapGroupsToRoleIds(groups: string[]): Promise { + if (!groups || groups.length === 0) return []; + const mappings = await (this.prisma as any).authGroupRoleMapping.findMany({ where: { authGroup: { in: groups } } }); + const roleIds = mappings.map((m: any) => m.roleId); + return Array.from(new Set(roleIds)); + } + + async syncUserRolesFromAuthentik(userId: string, groups: string[]) { + const groupHash = this.computeGroupHash(groups || []); + + const user = await (this.prisma as any).user.findUnique({ where: { id: userId } }); + if (!user) throw new Error('User not found'); + + if (user.lastGroupHash === groupHash) { + this.logger.debug('Group hash unchanged, skipping sync'); + return { skipped: true }; + } + + const roleIds = await this.mapGroupsToRoleIds(groups || []); + + const previousRoleRows = await (this.prisma as any).userRole.findMany({ where: { userId, source: 'AUTHENTIK' }, select: { roleId: true } }); + const previousRoles = previousRoleRows.map((r: any) => r.roleId); + + await (this.prisma as any).$transaction(async (tx: any) => { + await tx.userRole.deleteMany({ where: { userId: userId, source: 'AUTHENTIK' } }); + + if (roleIds.length > 0) { + const createData = roleIds.map((rid: string) => ({ userId, roleId: rid, source: 'AUTHENTIK' })); + await tx.userRole.createMany({ data: createData, skipDuplicates: true } as any); + } + + await tx.user.update({ where: { id: userId }, data: { lastGroupHash: groupHash } }); + }); + + // Invalidate permission cache through abstraction + try { + await this.permissionCache.invalidate(userId); + } catch (e) { + this.logger.error('Failed to invalidate permission cache', e as any); + } + + // Publish event for audit and other subscribers + this.events.publish({ + id: require('crypto').randomUUID(), + timestamp: new Date().toISOString(), + type: 'RolesSynchronized', + payload: { userId, groups, assignedRoleIds: roleIds, previousRoles }, + }); + + return { skipped: false, assignedRoleIds: roleIds }; + } +} diff --git a/src/modules/authorization/authorization.module.ts b/src/modules/authorization/authorization.module.ts new file mode 100644 index 0000000..ac85e4e --- /dev/null +++ b/src/modules/authorization/authorization.module.ts @@ -0,0 +1,16 @@ +import { Module } from '@nestjs/common'; +import { AuthorizationService, PERMISSION_CACHE } from './authorization.service'; +import { PrismaService } from '../../shared/prisma.service'; +import { RedisService } from '../../shared/redis.service'; +import { RedisPermissionCache } from './cache/redis-permission-cache.service'; + +@Module({ + providers: [ + AuthorizationService, + PrismaService, + RedisService, + { provide: PERMISSION_CACHE, useClass: RedisPermissionCache }, + ], + exports: [AuthorizationService, PERMISSION_CACHE], +}) +export class AuthorizationModule {} diff --git a/src/modules/authorization/authorization.service.ts b/src/modules/authorization/authorization.service.ts new file mode 100644 index 0000000..09f0f19 --- /dev/null +++ b/src/modules/authorization/authorization.service.ts @@ -0,0 +1,51 @@ +import { Injectable, Logger, Inject } from '@nestjs/common'; +import { PrismaService } from '../../shared/prisma.service'; +import { PermissionCache } from './cache/permission-cache.interface'; + +export const PERMISSION_CACHE = 'PERMISSION_CACHE'; + +@Injectable() +export class AuthorizationService { + private readonly logger = new Logger(AuthorizationService.name); + + constructor(private readonly prisma: PrismaService, @Inject(PERMISSION_CACHE) private readonly cache: PermissionCache) {} + + async getUserPermissions(userId: string): Promise { + try { + const cached = await this.cache.get(userId); + if (cached) return cached; + } catch (e) { + this.logger.debug('PermissionCache get failed', e as any); + } + + const rows = await this.prisma.$queryRaw` + SELECT p.code as code + FROM "UserRole" ur + JOIN "RolePermission" rp ON rp.role_id = ur.role_id + JOIN "Permission" p ON p.id = rp.permission_id + WHERE ur.user_id = ${userId}`; + + const perms = Array.isArray(rows) ? rows.map((r: any) => r.code) : []; + + try { + await this.cache.set(userId, perms); + } catch (e) { + this.logger.debug('PermissionCache set failed', e as any); + } + + return perms; + } + + async hasPermission(userId: string, permissionCode: string): Promise { + const perms = await this.getUserPermissions(userId); + return perms.includes(permissionCode); + } + + async invalidateUserPermissions(userId: string) { + try { + await this.cache.invalidate(userId); + } catch (e) { + this.logger.debug('PermissionCache invalidate failed', e as any); + } + } +} diff --git a/src/modules/authorization/cache/permission-cache.interface.ts b/src/modules/authorization/cache/permission-cache.interface.ts new file mode 100644 index 0000000..a8e8eab --- /dev/null +++ b/src/modules/authorization/cache/permission-cache.interface.ts @@ -0,0 +1,5 @@ +export interface PermissionCache { + get(userId: string): Promise; + set(userId: string, permissions: string[], ttlSeconds?: number): Promise; + invalidate(userId: string): Promise; +} diff --git a/src/modules/authorization/cache/redis-permission-cache.service.ts b/src/modules/authorization/cache/redis-permission-cache.service.ts new file mode 100644 index 0000000..1b290f6 --- /dev/null +++ b/src/modules/authorization/cache/redis-permission-cache.service.ts @@ -0,0 +1,33 @@ +import { Injectable } from '@nestjs/common'; +import { PermissionCache } from './permission-cache.interface'; +import { RedisService } from '../../../shared/redis.service'; + +@Injectable() +export class RedisPermissionCache implements PermissionCache { + private readonly TTL = 60 * 5; + + constructor(private readonly redis: RedisService) {} + + private key(userId: string) { + const { CacheKeys } = require('../../../shared/cache-keys'); + return CacheKeys.permission(userId); + } + + async get(userId: string): Promise { + const data = await this.redis.get(this.key(userId)); + if (!data) return null; + try { + return JSON.parse(data) as string[]; + } catch { + return null; + } + } + + async set(userId: string, permissions: string[], ttlSeconds?: number): Promise { + await this.redis.set(this.key(userId), JSON.stringify(permissions), ttlSeconds ?? this.TTL); + } + + async invalidate(userId: string): Promise { + await this.redis.del(this.key(userId)); + } +} diff --git a/src/modules/authorization/guards/permission.guard.ts b/src/modules/authorization/guards/permission.guard.ts new file mode 100644 index 0000000..0aa3e89 --- /dev/null +++ b/src/modules/authorization/guards/permission.guard.ts @@ -0,0 +1,28 @@ +import { CanActivate, ExecutionContext, Injectable, ForbiddenException } from '@nestjs/common'; +import { AuthorizationService } from '../authorization.service'; + +@Injectable() +export class PermissionGuard implements CanActivate { + constructor(private readonly authz: AuthorizationService, private readonly permission: string) {} + + async canActivate(context: ExecutionContext): Promise { + const req = context.switchToHttp().getRequest(); + const user = req.raylab?.user; + if (!user) throw new ForbiddenException('Missing user'); + + const allowed = await this.authz.hasPermission(user.id, this.permission); + if (!allowed) throw new ForbiddenException('Forbidden'); + return true; + } +} + +// Factory to create guard instances with permission string (used in decorators) +export const createPermissionGuard = (permission: string) => { + @Injectable() + class _Guard extends PermissionGuard { + constructor(authz: AuthorizationService) { + super(authz, permission); + } + } + return _Guard; +}; diff --git a/src/modules/authorization/presentation/guards/permission.guard.ts b/src/modules/authorization/presentation/guards/permission.guard.ts index acca065..bee1027 100644 --- a/src/modules/authorization/presentation/guards/permission.guard.ts +++ b/src/modules/authorization/presentation/guards/permission.guard.ts @@ -2,6 +2,8 @@ import { Injectable, CanActivate, ExecutionContext, + UnauthorizedException, + ForbiddenException, } from '@nestjs/common'; import { Reflector } from '@nestjs/core'; @@ -25,12 +27,20 @@ export class PermissionGuard implements CanActivate { return true; } - const request = context.switchToHttp().getRequest(); + const request = context.switchToHttp().getRequest() as any; - const user = request.user; + const user = request.currentUser; - return permissions.every(permission => - user.permissions.includes(permission), - ); + if (!user) { + throw new UnauthorizedException('Current user is missing.'); + } + + const hasAll = permissions.every((permission) => { + return user.hasPermission(permission); + }); + + if (!hasAll) throw new ForbiddenException('Insufficient permissions.'); + + return true; } } \ No newline at end of file diff --git a/src/modules/bot-debt/application/debt.service.ts b/src/modules/bot-debt/application/debt.service.ts new file mode 100644 index 0000000..744c4b3 --- /dev/null +++ b/src/modules/bot-debt/application/debt.service.ts @@ -0,0 +1,319 @@ +import { Injectable, BadRequestException, NotFoundException, ForbiddenException } from '@nestjs/common'; +import { PrismaService } from '../../../shared/prisma.service'; +import { randomBytes } from 'crypto'; +import { SyncIdentityHandler } from '../../identity/application/handlers/user/sync-identity.handler'; +import { IdentityData } from '../../../core/auth/interfaces/identity-data'; + +@Injectable() +export class DebtService { + constructor(private readonly prisma: PrismaService, private readonly syncIdentityHandler: SyncIdentityHandler) {} + + // User sync for Telegram mapping using SyncIdentityHandler + async syncUser(telegramUserId: number | string, displayName?: string) { + if (!telegramUserId) throw new BadRequestException('telegramUserId required'); + const sub = `telegram:${String(telegramUserId)}`; + const identity = new IdentityData(sub, displayName || undefined, undefined, { provider: 'telegram', telegramUserId: String(telegramUserId) }); + const user = await this.syncIdentityHandler.execute(identity as any); + // UserData has username/email; return display-friendly name as username + return { id: user.id, username: user.username, name: user.username }; + } + + // Group operations + async createGroup(ownerUserId: string, name: string) { + if (!name) throw new BadRequestException('name required'); + const publicId = await this.generatePublicId(); + const group = await this.prisma.debtGroup.create({ + data: { name, ownerId: ownerUserId, publicId }, + }); + // add owner as member + await this.prisma.debtGroupMember.create({ data: { groupId: group.id, userId: ownerUserId } }); + return { id: group.id, publicId: group.publicId, name: group.name }; + } + + async listGroupsForUser(userId: string) { + const memberships = await this.prisma.debtGroupMember.findMany({ where: { userId }, include: { group: true } }); + return memberships.map(m => ({ id: m.group.id, publicId: m.group.publicId, name: m.group.name, ownerId: m.group.ownerId })); + } + + async getGroupDetail(userId: string, id: string) { + const group = await this.prisma.debtGroup.findUnique({ where: { id }, include: { members: true, people: true } }); + if (!group) throw new NotFoundException('Group not found'); + // check membership + const isMember = await this.prisma.debtGroupMember.findUnique({ where: { groupId_userId: { groupId: id, userId } } }); + if (!isMember) throw new ForbiddenException('Not a member'); + return { id: group.id, publicId: group.publicId, name: group.name, people: group.people.map(p => ({ id: p.id, name: p.name })) }; + } + + async joinGroupByPublicId(userId: string, publicId: string) { + const group = await this.prisma.debtGroup.findUnique({ where: { publicId } }); + if (!group) throw new NotFoundException('Group not found'); + try { + const member = await this.prisma.debtGroupMember.create({ data: { groupId: group.id, userId } }); + return { id: member.id }; + } catch (e) { + // unique constraint -> already member + return { ok: true }; + } + } + + async leaveGroup(userId: string, groupId: string) { + const group = await this.prisma.debtGroup.findUnique({ where: { id: groupId } }); + if (!group) throw new NotFoundException('Group not found'); + await this.prisma.debtGroupMember.deleteMany({ where: { groupId, userId } }); + return { ok: true }; + } + + // Person operations (owner only) + async addPerson(userId: string, groupId: string, name: string) { + const group = await this.prisma.debtGroup.findUnique({ where: { id: groupId } }); + if (!group) throw new NotFoundException('Group not found'); + if (group.ownerId !== userId) throw new ForbiddenException('Only owner can add person'); + // unique name per group + try { + const person = await this.prisma.debtPerson.create({ data: { groupId, name } }); + return { id: person.id, name: person.name }; + } catch (e) { + throw new BadRequestException('Person with same name already exists in group'); + } + } + + async listPeople(userId: string, groupId: string) { + const member = await this.prisma.debtGroupMember.findUnique({ where: { groupId_userId: { groupId, userId } } }); + if (!member) throw new ForbiddenException('Not a member'); + const people = await this.prisma.debtPerson.findMany({ where: { groupId, isDeleted: false } }); + return people.map(p => ({ id: p.id, name: p.name })); + } + + async removePerson(userId: string, groupId: string, personId: string) { + const group = await this.prisma.debtGroup.findUnique({ where: { id: groupId } }); + if (!group) throw new NotFoundException('Group not found'); + if (group.ownerId !== userId) throw new ForbiddenException('Only owner can remove person'); + // soft delete + await this.prisma.debtPerson.update({ where: { id: personId }, data: { isDeleted: true } }); + return { ok: true }; + } + + // Transaction creation + private parseAmount(raw: string | number) { + if (typeof raw === 'number') return BigInt(raw); + if (typeof raw !== 'string') throw new BadRequestException('Nominal tidak valid. Gunakan format seperti 17000 atau 17.000.'); + const normalized = raw.replace(/\./g, ''); + if (!/^\d+$/.test(normalized)) throw new BadRequestException('Nominal tidak valid. Gunakan format seperti 17000 atau 17.000.'); + const value = BigInt(normalized); + return value; + } + + private parseDate(dateStr?: string) { + const tz = 'Asia/Jakarta'; + if (!dateStr) { + // current date in Asia/Jakarta + const now = new Date(); + const local = new Date(now.toLocaleString('en-US', { timeZone: 'Asia/Jakarta' })); + // keep date part only + local.setHours(0, 0, 0, 0); + return local; + } + // format yyyy/mm/dd + if (!/^\d{4}\/\d{2}\/\d{2}$/.test(dateStr)) throw new BadRequestException('Tanggal tidak valid. Gunakan format yyyy/mm/dd'); + const [y, m, d] = dateStr.split('/').map(s => parseInt(s, 10)); + // create Date in Asia/Jakarta by constructing as if local time then setHours 0 + const dt = new Date(Date.UTC(y, m - 1, d)); + // store as date-only; prisma maps to date + return dt; + } + + async createTransaction(userId: string, groupId: string, type: 'DEBT' | 'PAYMENT', body: any) { + const { from, to, price, description, date, requestId } = body; + if (!from || !to) throw new BadRequestException('from and to required'); + if (from === to) throw new BadRequestException('from and to cannot be same'); + // validate group exists and member + const group = await this.prisma.debtGroup.findUnique({ where: { id: groupId } }); + if (!group) throw new NotFoundException('Group not found'); + const membership = await this.prisma.debtGroupMember.findUnique({ where: { groupId_userId: { groupId, userId } } }); + if (!membership) throw new ForbiddenException('Not a member'); + + // find persons by name within group + const fromPerson = await this.prisma.debtPerson.findFirst({ where: { groupId, name: from, isDeleted: false } }); + const toPerson = await this.prisma.debtPerson.findFirst({ where: { groupId, name: to, isDeleted: false } }); + if (!fromPerson || !toPerson) throw new BadRequestException('Person not found in group'); + + if (!requestId) throw new BadRequestException('requestId is required for idempotency'); + + // idempotency: check existing in DB + const existing = await this.prisma.debtTransaction.findFirst({ where: { groupId, requestId } }); + if (existing) return existing; + + const amount = this.parseAmount(price); + if (amount <= 0n) throw new BadRequestException('Nominal harus lebih besar dari 0'); + const txDate = this.parseDate(date); + + if (type === 'DEBT') { + const created = await this.prisma.debtTransaction.create({ + data: { + groupId, + fromPersonId: fromPerson.id, + toPersonId: toPerson.id, + amount, + type, + description, + transactionDate: txDate, + createdByUserId: userId, + requestId, + }, + }); + + return created; + } + + // PAYMENT handling with overpayment -> create reverse debt for remainder + if (type === 'PAYMENT') { + // compute current net (positive means from owes to) + const txs = await this.prisma.debtTransaction.findMany({ where: { groupId, OR: [{ fromPersonId: fromPerson.id, toPersonId: toPerson.id }, { fromPersonId: toPerson.id, toPersonId: fromPerson.id }] } }); + let net = 0n; + for (const t of txs) { + if (t.fromPersonId === fromPerson.id && t.toPersonId === toPerson.id) { + net += (t.type === 'DEBT' ? BigInt(t.amount) : -BigInt(t.amount)); + } else if (t.fromPersonId === toPerson.id && t.toPersonId === fromPerson.id) { + net += (t.type === 'DEBT' ? -BigInt(t.amount) : BigInt(t.amount)); + } + } + + // create PAYMENT transaction + const payment = await this.prisma.debtTransaction.create({ + data: { + groupId, + fromPersonId: fromPerson.id, + toPersonId: toPerson.id, + amount, + type, + description, + transactionDate: txDate, + createdByUserId: userId, + requestId, + }, + }); + + // existing positive debt (from owes to) + const existingPositive = net > 0n ? net : 0n; + const remainder = amount - existingPositive; + + if (remainder > 0n) { + // need creator name for description + const creator = await this.prisma.user.findUnique({ where: { id: userId } }); + const creatorName = creator?.name || creator?.username || userId; + const rev = await this.prisma.debtTransaction.create({ + data: { + groupId, + fromPersonId: toPerson.id, + toPersonId: fromPerson.id, + amount: remainder, + type: 'DEBT', + description: `Sisa pembayaran hutang dari ${creatorName}`, + transactionDate: txDate, + createdByUserId: userId, + }, + }); + + return { payment, remainder: rev }; + } + + return payment; + } + + throw new BadRequestException('Invalid transaction type'); + } + + async listTransactions(userId: string, groupId: string, page = 1, limit = 100) { + const membership = await this.prisma.debtGroupMember.findUnique({ where: { groupId_userId: { groupId, userId } } }); + if (!membership) throw new ForbiddenException('Not a member'); + const txs = await this.prisma.debtTransaction.findMany({ where: { groupId }, orderBy: [{ transactionDate: 'asc' }, { createdAt: 'asc' }], include: { fromPerson: true, toPerson: true, createdByUser: true } }); + return txs.map(t => ({ id: t.id, from: t.fromPerson.name, to: t.toPerson.name, amount: t.amount.toString(), type: t.type, description: t.description, transactionDate: t.transactionDate, createdAt: t.createdAt })); + } + + async getDetail(userId: string, groupId: string) { + // history same as listTransactions + return this.listTransactions(userId, groupId); + } + + async getSummary(userId: string, groupId: string) { + const membership = await this.prisma.debtGroupMember.findUnique({ where: { groupId_userId: { groupId, userId } } }); + if (!membership) throw new ForbiddenException('Not a member'); + const people = await this.prisma.debtPerson.findMany({ where: { groupId, isDeleted: false } }); + const personIds = people.map(p => p.id); + const txs = await this.prisma.debtTransaction.findMany({ where: { groupId } }); + // compute pairwise net: map fromId->toId -> bigint + const pairMap = new Map(); + const nameById = new Map(); + people.forEach(p => nameById.set(p.id, p.name)); + + for (const t of txs) { + const key = `${t.fromPersonId}::${t.toPersonId}`; + const prev = pairMap.get(key) || 0n; + if (t.type === 'DEBT') { + pairMap.set(key, prev + BigInt(t.amount)); + } else { + pairMap.set(key, prev - BigInt(t.amount)); + } + } + + // collapse pairwise into net per pair (from->to positive means from owes to) + const netMap = new Map(); + for (const [key, val] of pairMap.entries()) { + const [a, b] = key.split('::'); + const reverseKey = `${b}::${a}`; + const rev = pairMap.get(reverseKey) || 0n; + const net = (val - rev); + if (net === 0n) continue; + if (net > 0n) { + netMap.set(`${a}::${b}`, net); + } else { + netMap.set(`${b}::${a}`, -net); + } + } + + // Now perform global netting using net balances + const balance = new Map(); + for (const p of people) balance.set(p.id, 0n); + + for (const [k, v] of netMap.entries()) { + const [a, b] = k.split('::'); + balance.set(a, (balance.get(a) || 0n) - v); + balance.set(b, (balance.get(b) || 0n) + v); + } + + // Greedy settlement + const settlements: Array<{ from: string; to: string; amount: string }> = []; + const creditors = [] as Array<{ id: string; bal: bigint }>; + const debtors = [] as Array<{ id: string; bal: bigint }>; + balance.forEach((v, k) => { + if (v > 0n) creditors.push({ id: k, bal: v }); + else if (v < 0n) debtors.push({ id: k, bal: -v }); + }); + creditors.sort((a, b) => Number(b.bal - a.bal)); + debtors.sort((a, b) => Number(b.bal - a.bal)); + + let i = 0; + let j = 0; + while (i < debtors.length && j < creditors.length) { + const debtor = debtors[i]; + const creditor = creditors[j]; + const settle = debtor.bal < creditor.bal ? debtor.bal : creditor.bal; + settlements.push({ from: nameById.get(debtor.id) || debtor.id, to: nameById.get(creditor.id) || creditor.id, amount: settle.toString() }); + debtor.bal -= settle; + creditor.bal -= settle; + if (debtor.bal === 0n) i++; + if (creditor.bal === 0n) j++; + } + + return settlements; + } + + private async generatePublicId(): Promise { + // RL-XXXXXX style + const code = 'RL-' + randomBytes(4).toString('hex').toUpperCase().slice(0, 6); + // ensure uniqueness + const exists = await this.prisma.debtGroup.findUnique({ where: { publicId: code } }); + if (exists) return this.generatePublicId(); + return code; + } +} diff --git a/src/modules/bot-debt/debt.module.ts b/src/modules/bot-debt/debt.module.ts new file mode 100644 index 0000000..dade393 --- /dev/null +++ b/src/modules/bot-debt/debt.module.ts @@ -0,0 +1,14 @@ +import { Module } from '@nestjs/common'; +import { PrismaService } from '../../shared/prisma.service'; +import { JwtAuthGuard } from '../../core/auth/guards/jwt-auth.guard'; +import { DebtController } from './presentation/debt.controller'; +import { DebtService } from './application/debt.service'; +import { IdentityModule } from '../identity/identity.module'; + +@Module({ + imports: [IdentityModule], + controllers: [DebtController], + providers: [PrismaService, DebtService, JwtAuthGuard], + exports: [DebtService], +}) +export class DebtModule {} diff --git a/src/modules/bot-debt/presentation/debt.controller.ts b/src/modules/bot-debt/presentation/debt.controller.ts new file mode 100644 index 0000000..c2ceaf0 --- /dev/null +++ b/src/modules/bot-debt/presentation/debt.controller.ts @@ -0,0 +1,114 @@ +import { Controller, Post, Body, UseGuards, Req, Get, Param, Patch, Delete, Query } from '@nestjs/common'; +import { JwtAuthGuard } from '../../../core/auth/guards/jwt-auth.guard'; +import { CurrentUserGuard } from '../../identity/presentation/guards/current-user.guard'; +import { DebtService } from '../application/debt.service'; +import { SyncUserDto } from './dto/sync-user.dto'; +import { CreateGroupDto } from './dto/create-group.dto'; +import { CreatePersonDto } from './dto/create-person.dto'; +import { CreateTransactionDto } from './dto/create-transaction.dto'; + +@Controller('api/bot/debt') +@UseGuards(JwtAuthGuard, CurrentUserGuard) +export class DebtController { + constructor(private readonly debtService: DebtService) {} + + @Post('users/sync') + async syncUser(@Body() body: SyncUserDto) { + const { telegramUserId, displayName } = body; + const user = await this.debtService.syncUser(telegramUserId, displayName); + return { success: true, data: user }; + } + + @Post('groups') + async createGroup(@Req() req: any, @Body() body: CreateGroupDto) { + const userId = req.currentUser.id; + const { name } = body; + const group = await this.debtService.createGroup(userId, name); + return { success: true, data: group }; + } + + @Get('groups') + async listGroups(@Req() req: any) { + const userId = req.currentUser.id; + const groups = await this.debtService.listGroupsForUser(userId); + return { success: true, data: groups }; + } + + @Get('groups/:id') + async getGroup(@Req() req: any, @Param('id') id: string) { + const userId = req.currentUser.id; + const group = await this.debtService.getGroupDetail(userId, id); + return { success: true, data: group }; + } + + @Post('groups/:publicId/join') + async joinGroup(@Req() req: any, @Param('publicId') publicId: string) { + const userId = req.currentUser.id; + const res = await this.debtService.joinGroupByPublicId(userId, publicId); + return { success: true, data: res }; + } + + @Post('groups/:id/leave') + async leaveGroup(@Req() req: any, @Param('id') id: string) { + const userId = req.currentUser.id; + const res = await this.debtService.leaveGroup(userId, id); + return { success: true, data: res }; + } + + @Post('groups/:id/people') + async addPerson(@Req() req: any, @Param('id') id: string, @Body() body: CreatePersonDto) { + const userId = req.currentUser.id; + const { name } = body; + const person = await this.debtService.addPerson(userId, id, name); + return { success: true, data: person }; + } + + @Get('groups/:id/people') + async listPeople(@Req() req: any, @Param('id') id: string) { + const userId = req.currentUser.id; + const people = await this.debtService.listPeople(userId, id); + return { success: true, data: people }; + } + + @Delete('groups/:id/people/:personId') + async removePerson(@Req() req: any, @Param('id') id: string, @Param('personId') personId: string) { + const userId = req.currentUser.id; + await this.debtService.removePerson(userId, id, personId); + return { success: true, data: null }; + } + + @Post('groups/:id/transactions/debt') + async addDebt(@Req() req: any, @Param('id') id: string, @Body() body: CreateTransactionDto) { + const userId = req.currentUser.id; + const res = await this.debtService.createTransaction(userId, id, 'DEBT', body); + return { success: true, data: res }; + } + + @Post('groups/:id/transactions/payment') + async addPayment(@Req() req: any, @Param('id') id: string, @Body() body: CreateTransactionDto) { + const userId = req.currentUser.id; + const res = await this.debtService.createTransaction(userId, id, 'PAYMENT', body); + return { success: true, data: res }; + } + + @Get('groups/:id/transactions') + async listTransactions(@Req() req: any, @Param('id') id: string, @Query('page') page = 1, @Query('limit') limit = 100) { + const userId = req.currentUser.id; + const res = await this.debtService.listTransactions(userId, id, Number(page), Number(limit)); + return { success: true, data: res }; + } + + @Get('groups/:id/summary') + async summary(@Req() req: any, @Param('id') id: string) { + const userId = req.currentUser.id; + const res = await this.debtService.getSummary(userId, id); + return { success: true, data: res }; + } + + @Get('groups/:id/detail') + async detail(@Req() req: any, @Param('id') id: string) { + const userId = req.currentUser.id; + const res = await this.debtService.getDetail(userId, id); + return { success: true, data: res }; + } +} diff --git a/src/modules/bot-debt/presentation/dto/create-group.dto.ts b/src/modules/bot-debt/presentation/dto/create-group.dto.ts new file mode 100644 index 0000000..8d21e2f --- /dev/null +++ b/src/modules/bot-debt/presentation/dto/create-group.dto.ts @@ -0,0 +1,7 @@ +import { IsNotEmpty, IsString } from 'class-validator'; + +export class CreateGroupDto { + @IsString() + @IsNotEmpty() + name!: string; +} diff --git a/src/modules/bot-debt/presentation/dto/create-person.dto.ts b/src/modules/bot-debt/presentation/dto/create-person.dto.ts new file mode 100644 index 0000000..efcd49c --- /dev/null +++ b/src/modules/bot-debt/presentation/dto/create-person.dto.ts @@ -0,0 +1,7 @@ +import { IsNotEmpty, IsString } from 'class-validator'; + +export class CreatePersonDto { + @IsNotEmpty() + @IsString() + name!: string; +} diff --git a/src/modules/bot-debt/presentation/dto/create-transaction.dto.ts b/src/modules/bot-debt/presentation/dto/create-transaction.dto.ts new file mode 100644 index 0000000..1beb069 --- /dev/null +++ b/src/modules/bot-debt/presentation/dto/create-transaction.dto.ts @@ -0,0 +1,31 @@ +import { IsNotEmpty, IsString, Matches, IsOptional } from 'class-validator'; + +export class CreateTransactionDto { + @IsNotEmpty() + @IsString() + from!: string; + + @IsNotEmpty() + @IsString() + to!: string; + + @IsNotEmpty() + @IsString() + // Accept either plain digits or dot separated thousands: e.g. 17000 or 17.000 + @Matches(/^(?:\d+|\d{1,3}(?:\.\d{3})+)$/) + price!: string; + + @IsOptional() + @IsString() + description?: string; + + @IsOptional() + @IsString() + // yyyy/mm/dd + @Matches(/^\d{4}\/\d{2}\/\d{2}$/) + date?: string; + + @IsNotEmpty() + @IsString() + requestId!: string; +} diff --git a/src/modules/bot-debt/presentation/dto/sync-user.dto.ts b/src/modules/bot-debt/presentation/dto/sync-user.dto.ts new file mode 100644 index 0000000..56ed0cd --- /dev/null +++ b/src/modules/bot-debt/presentation/dto/sync-user.dto.ts @@ -0,0 +1,10 @@ +import { IsNotEmpty, IsString } from 'class-validator'; + +export class SyncUserDto { + @IsNotEmpty() + @IsString() + telegramUserId!: string; + + @IsString() + displayName?: string; +} diff --git a/src/modules/health/health.controller.ts b/src/modules/health/health.controller.ts new file mode 100644 index 0000000..239c869 --- /dev/null +++ b/src/modules/health/health.controller.ts @@ -0,0 +1,12 @@ +import { Controller, Get } from '@nestjs/common'; +import { HealthService } from './health.service'; + +@Controller('api/health') +export class HealthController { + constructor(private readonly healthService: HealthService) {} + + @Get() + async get() { + return this.healthService.getHealth(); + } +} diff --git a/src/modules/health/health.module.ts b/src/modules/health/health.module.ts new file mode 100644 index 0000000..a38cb2c --- /dev/null +++ b/src/modules/health/health.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { HealthController } from './health.controller'; +import { HealthService } from './health.service'; + +@Module({ + controllers: [HealthController], + providers: [HealthService], + exports: [HealthService], +}) +export class HealthModule {} diff --git a/src/modules/health/health.service.ts b/src/modules/health/health.service.ts new file mode 100644 index 0000000..aca7ef4 --- /dev/null +++ b/src/modules/health/health.service.ts @@ -0,0 +1,12 @@ +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class HealthService { + async getHealth() { + return { + status: 'ok', + timestamp: new Date().toISOString(), + service: 'raylab-core', + }; + } +} diff --git a/src/modules/identity/application/README.md b/src/modules/identity/application/README.md index e01622b..68f05f1 100644 --- a/src/modules/identity/application/README.md +++ b/src/modules/identity/application/README.md @@ -1,12 +1 @@ -modules/identity/application/ - -Penjelasan: -Layer application berisi use-case (services/commands/queries) yang mengorkestrasi domain dan infrastruktur. - -Contoh file: -- services/get-user.service.ts -- commands/create-user.command.ts - -Aturan: -- Application service boleh memanggil repository interface, domain services, dan event publisher. -- Application menangani transaksi jika diperlukan. +ini adalah manager, semua kegiatan diarahkan dari sini, disini bukan mengakses db, menghitung, dll, dari controller akan masuk ke dalam sini dan dijalankan prosesnya, tapi tidak tau http dan databasenya. \ No newline at end of file diff --git a/src/modules/identity/application/config/env-auth-config.ts b/src/modules/identity/application/config/env-auth-config.ts new file mode 100644 index 0000000..74b24ed --- /dev/null +++ b/src/modules/identity/application/config/env-auth-config.ts @@ -0,0 +1,15 @@ +import { IAuthConfig } from './i-auth-config'; + +export class EnvAuthConfig implements IAuthConfig { + autoCreateUser(): boolean { + return (process.env.AUTH_AUTO_CREATE_USER ?? 'true') === 'true'; + } + + syncEmail(): boolean { + return (process.env.AUTH_SYNC_EMAIL ?? 'true') === 'true'; + } + + syncUsername(): boolean { + return (process.env.AUTH_SYNC_USERNAME ?? 'false') === 'true'; + } +} diff --git a/src/modules/identity/application/config/i-auth-config.ts b/src/modules/identity/application/config/i-auth-config.ts new file mode 100644 index 0000000..8e1d6a5 --- /dev/null +++ b/src/modules/identity/application/config/i-auth-config.ts @@ -0,0 +1,5 @@ +export abstract class IAuthConfig { + abstract autoCreateUser(): boolean; + abstract syncEmail(): boolean; + abstract syncUsername(): boolean; +} diff --git a/src/modules/identity/application/handlers/README.MD b/src/modules/identity/application/handlers/README.MD new file mode 100644 index 0000000..a0639ca --- /dev/null +++ b/src/modules/identity/application/handlers/README.MD @@ -0,0 +1 @@ +semua method disini didapat dari interface, itu ada di domain/repositories. \ No newline at end of file diff --git a/src/modules/identity/application/handlers/delete-user.handler.ts b/src/modules/identity/application/handlers/delete-user.handler.ts deleted file mode 100644 index be70def..0000000 --- a/src/modules/identity/application/handlers/delete-user.handler.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { - Injectable, - NotFoundException, -} from '@nestjs/common'; - -import { UserRepository } from '../../domain/repositories/user.repository.interface'; - -@Injectable() -export class DeleteUserHandler { - constructor( - private readonly userRepository: UserRepository, - ) {} - - async execute(id: string): Promise { - const user = await this.userRepository.findById(id); - - if (!user) { - throw new NotFoundException('User not found.'); - } - - user.delete(); - - await this.userRepository.update(user); - - // TODO: - // Publish UserDeletedEvent - } -} \ No newline at end of file diff --git a/src/modules/identity/application/handlers/find-user.handler.ts b/src/modules/identity/application/handlers/find-user.handler.ts deleted file mode 100644 index 5942fb4..0000000 --- a/src/modules/identity/application/handlers/find-user.handler.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { - Injectable, - NotFoundException, -} from '@nestjs/common'; - -import { UserRepository } from '../../domain/repositories/user.repository.interface'; -import { User } from '../../domain/entities/user.entity'; - -@Injectable() -export class FindUserHandler { - constructor( - private readonly userRepository: UserRepository, - ) {} - - async execute(id: string): Promise { - const user = await this.userRepository.findById(id); - - if (!user) { - throw new NotFoundException('User not found.'); - } - - return user; - } -} \ No newline at end of file diff --git a/src/modules/identity/application/handlers/find-users.handler.ts b/src/modules/identity/application/handlers/find-users.handler.ts deleted file mode 100644 index c7b3af1..0000000 --- a/src/modules/identity/application/handlers/find-users.handler.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -import { UserRepository } from '../../domain/repositories/user.repository.interface'; -import { User } from '../../domain/entities/user.entity'; - -@Injectable() -export class FindUsersHandler { - constructor( - private readonly userRepository: UserRepository, - ) {} - - async execute(): Promise { - return await this.userRepository.findAll(); - } -} \ No newline at end of file diff --git a/src/modules/identity/application/handlers/permission/create-permission.handler.ts b/src/modules/identity/application/handlers/permission/create-permission.handler.ts new file mode 100644 index 0000000..07f1452 --- /dev/null +++ b/src/modules/identity/application/handlers/permission/create-permission.handler.ts @@ -0,0 +1,29 @@ +import { Injectable, ConflictException } from '@nestjs/common'; +import { IPermission } from '../../../domain/repositories/permission.interface'; +import { PermissionData } from '../../../domain/entities/permission.entity'; +import { EventBus } from '../../../../../core/event-bus/event-bus.service'; + +const crypto = require('crypto'); + +@Injectable() +export class CreatePermissionHandler { + constructor(private readonly permissionRepository: IPermission, private readonly events: EventBus) {} + + async execute(dto: any) { + if (!dto.name || !dto.code) throw new ConflictException('Missing required fields'); + + const perm = PermissionData.restore({ + id: crypto.randomUUID(), + code: dto.code || dto.name, + name: dto.name, + displayName: dto.displayName || dto.name, + description: dto.description || '', + createdAt: new Date(), + updatedAt: new Date(), + } as any); + + const created = await this.permissionRepository.create(perm); + this.events.publish({ id: require('crypto').randomUUID(), timestamp: new Date().toISOString(), type: 'PermissionCreated', payload: { permissionId: created.id } }); + return created; + } +} diff --git a/src/modules/identity/application/handlers/permission/delete-permission.handler.ts b/src/modules/identity/application/handlers/permission/delete-permission.handler.ts new file mode 100644 index 0000000..ad9b8db --- /dev/null +++ b/src/modules/identity/application/handlers/permission/delete-permission.handler.ts @@ -0,0 +1,34 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; +import { IPermission } from '../../../domain/repositories/permission.interface'; +import { IRole } from '../../../domain/repositories/role.interface'; +import { AuthorizationService } from '../../../../authorization/authorization.service'; +import { EventBus } from '../../../../../core/event-bus/event-bus.service'; + +@Injectable() +export class DeletePermissionHandler { + constructor(private readonly permissionRepository: IPermission, private readonly roleRepository: IRole, private readonly authorizationService: AuthorizationService, private readonly events: EventBus) {} + + async execute(id: string) { + const p = await this.permissionRepository.getById(id); + if (!p) throw new NotFoundException('Permission not found.'); + + // find roles that reference this permission + const rolesWithPerm = await (this.permissionRepository as any).findRoleIdsByPermission(id); + + // delete permission + await this.permissionRepository.delete(id); + + // invalidate caches for users who have affected roles + const userSet = new Set(); + for (const rid of rolesWithPerm) { + const uids = await this.roleRepository.getAssignedUserIds(rid); + uids.forEach(u => userSet.add(u)); + } + + for (const uid of Array.from(userSet)) await this.authorizationService.invalidateUserPermissions(uid); + + this.events.publish({ id: require('crypto').randomUUID(), timestamp: new Date().toISOString(), type: 'PermissionDeleted', payload: { permissionId: id, affectedRoles: rolesWithPerm } }); + + return; + } +} diff --git a/src/modules/identity/application/handlers/permission/get-permission.handler.ts b/src/modules/identity/application/handlers/permission/get-permission.handler.ts new file mode 100644 index 0000000..c7a1c8c --- /dev/null +++ b/src/modules/identity/application/handlers/permission/get-permission.handler.ts @@ -0,0 +1,11 @@ +import { Injectable } from '@nestjs/common'; +import { IPermission } from '../../../domain/repositories/permission.interface'; + +@Injectable() +export class GetPermissionHandler { + constructor(private readonly permissionRepository: IPermission) {} + + async execute(id: string) { + return this.permissionRepository.getById(id); + } +} diff --git a/src/modules/identity/application/handlers/permission/get-permissions.handler.ts b/src/modules/identity/application/handlers/permission/get-permissions.handler.ts new file mode 100644 index 0000000..309301b --- /dev/null +++ b/src/modules/identity/application/handlers/permission/get-permissions.handler.ts @@ -0,0 +1,11 @@ +import { Injectable } from '@nestjs/common'; +import { IPermission } from '../../../domain/repositories/permission.interface'; + +@Injectable() +export class GetPermissionsHandler { + constructor(private readonly permissionRepository: IPermission) {} + + async execute(query: { page?: number; limit?: number; search?: string }) { + return this.permissionRepository.find({ page: query.page, limit: query.limit, search: query.search || null }); + } +} diff --git a/src/modules/identity/application/handlers/permission/update-permission.handler.ts b/src/modules/identity/application/handlers/permission/update-permission.handler.ts new file mode 100644 index 0000000..f5390c6 --- /dev/null +++ b/src/modules/identity/application/handlers/permission/update-permission.handler.ts @@ -0,0 +1,36 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; +import { IPermission } from '../../../domain/repositories/permission.interface'; +import { IRole } from '../../../domain/repositories/role.interface'; +import { AuthorizationService } from '../../../../authorization/authorization.service'; +import { EventBus } from '../../../../../core/event-bus/event-bus.service'; + +@Injectable() +export class UpdatePermissionHandler { + constructor(private readonly permissionRepository: IPermission, private readonly roleRepository: IRole, private readonly authorizationService: AuthorizationService, private readonly events: EventBus) {} + + async execute(id: string, dto: any) { + const perm = await this.permissionRepository.getById(id); + if (!perm) throw new NotFoundException('Permission not found.'); + + if (dto.name) perm.changeName(dto.name); + if (dto.description !== undefined) perm.changeDescription(dto.description); + if (dto.code) perm.changeCode(dto.code); + + const updated = await this.permissionRepository.update(perm); + + // invalidate caches for users who belong to roles that reference this permission + const roleIds = await (this.permissionRepository as any).findRoleIdsByPermission(id); + const userSet = new Set(); + + for (const rid of roleIds) { + const uids = await this.roleRepository.getAssignedUserIds(rid); + uids.forEach(u => userSet.add(u)); + } + + for (const uid of Array.from(userSet)) await this.authorizationService.invalidateUserPermissions(uid); + + this.events.publish({ id: require('crypto').randomUUID(), timestamp: new Date().toISOString(), type: 'PermissionUpdated', payload: { permissionId: id } }); + + return updated; + } +} diff --git a/src/modules/identity/application/handlers/role/assign-permission.handler.ts b/src/modules/identity/application/handlers/role/assign-permission.handler.ts new file mode 100644 index 0000000..f5f005d --- /dev/null +++ b/src/modules/identity/application/handlers/role/assign-permission.handler.ts @@ -0,0 +1,38 @@ +import { Injectable, NotFoundException, Inject } from '@nestjs/common'; +import { IRole } from '../../../domain/repositories/role.interface'; +import { IPermission } from '../../../domain/repositories/permission.interface'; +import { AuthorizationService } from '../../../../authorization/authorization.service'; +import { EventBus } from '../../../../../core/event-bus/event-bus.service'; + +@Injectable() +export class RoleAssignPermissionHandler { + constructor( + private readonly roleRepository: IRole, + private readonly permissionRepository: IPermission, + private readonly authorizationService: AuthorizationService, + private readonly events: EventBus, + ) {} + + async execute(roleId: string, permissionId: string) { + const role = await this.roleRepository.findById(roleId); + if (!role) throw new NotFoundException('Role not found.'); + + const perm = await this.permissionRepository.getById(permissionId); + if (!perm) throw new NotFoundException('Permission not found.'); + + role.assignPermission(perm); + + const updated = await this.roleRepository.update(role); + + // Invalidate permissions cache for users who have this role + const userIds = await this.roleRepository.getAssignedUserIds(roleId); + for (const uid of userIds) { + await this.authorizationService.invalidateUserPermissions(uid); + } + + // Publish role.updated event + this.events.publish({ id: require('crypto').randomUUID(), timestamp: new Date().toISOString(), type: 'RoleUpdated', payload: { roleId, permissionId } }); + + return updated; + } +} diff --git a/src/modules/identity/application/handlers/role/create-role.handler.ts b/src/modules/identity/application/handlers/role/create-role.handler.ts new file mode 100644 index 0000000..842186f --- /dev/null +++ b/src/modules/identity/application/handlers/role/create-role.handler.ts @@ -0,0 +1,37 @@ +import { Injectable, ConflictException } from '@nestjs/common'; +import { IRole } from '../../../domain/repositories/role.interface'; +import { RoleData } from '../../../domain/entities/role.entity'; +import { EventBus } from '../../../../../core/event-bus/event-bus.service'; + +const crypto = require('crypto'); + +@Injectable() +export class CreateRoleHandler { + constructor(private readonly roleRepository: IRole, private readonly events: EventBus) {} + + async execute(dto: any) { + // basic validation + if (!dto.code || !dto.name) throw new ConflictException('Missing required fields'); + + try { + const role = RoleData.restore({ + id: crypto.randomUUID(), + code: dto.code, + name: dto.name, + description: dto.description || '', + permissions: [], + isDefault: dto.isDefault || false, + createdAt: new Date(), + updatedAt: new Date(), + } as any); + + const created = await this.roleRepository.create(role); + + this.events.publish({ id: require('crypto').randomUUID(), timestamp: new Date().toISOString(), type: 'RoleCreated', payload: { roleId: created.id } }); + + return created; + } catch (e) { + throw new ConflictException('Role creation failed.'); + } + } +} diff --git a/src/modules/identity/application/handlers/role/delete-role.handler.ts b/src/modules/identity/application/handlers/role/delete-role.handler.ts new file mode 100644 index 0000000..770ba07 --- /dev/null +++ b/src/modules/identity/application/handlers/role/delete-role.handler.ts @@ -0,0 +1,26 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; +import { IRole } from '../../../domain/repositories/role.interface'; +import { AuthorizationService } from '../../../../authorization/authorization.service'; +import { EventBus } from '../../../../../core/event-bus/event-bus.service'; + +@Injectable() +export class DeleteRoleHandler { + constructor(private readonly roleRepository: IRole, private readonly authorizationService: AuthorizationService, private readonly events: EventBus) {} + + async execute(id: string) { + const role = await this.roleRepository.findById(id); + if (!role) throw new NotFoundException('Role not found.'); + + // get affected users before delete + const userIds = await this.roleRepository.getAssignedUserIds(id); + + await this.roleRepository.delete(id); + + // invalidate caches + for (const uid of userIds) await this.authorizationService.invalidateUserPermissions(uid); + + this.events.publish({ id: require('crypto').randomUUID(), timestamp: new Date().toISOString(), type: 'RoleDeleted', payload: { roleId: id } }); + + return; + } +} diff --git a/src/modules/identity/application/handlers/role/get-role.handler.ts b/src/modules/identity/application/handlers/role/get-role.handler.ts new file mode 100644 index 0000000..ea0df1c --- /dev/null +++ b/src/modules/identity/application/handlers/role/get-role.handler.ts @@ -0,0 +1,11 @@ +import { Injectable } from '@nestjs/common'; +import { IRole } from '../../../domain/repositories/role.interface'; + +@Injectable() +export class GetRoleHandler { + constructor(private readonly roleRepository: IRole) {} + + async execute(id: string) { + return this.roleRepository.findById(id); + } +} diff --git a/src/modules/identity/application/handlers/role/get-roles.handler.ts b/src/modules/identity/application/handlers/role/get-roles.handler.ts new file mode 100644 index 0000000..a931691 --- /dev/null +++ b/src/modules/identity/application/handlers/role/get-roles.handler.ts @@ -0,0 +1,11 @@ +import { Injectable } from '@nestjs/common'; +import { IRole } from '../../../domain/repositories/role.interface'; + +@Injectable() +export class GetRolesHandler { + constructor(private readonly roleRepository: IRole) {} + + async execute(query: { page?: number; limit?: number; search?: string }) { + return this.roleRepository.find({ page: query.page, limit: query.limit, search: query.search || null }); + } +} diff --git a/src/modules/identity/application/handlers/role/remove-permission.handler.ts b/src/modules/identity/application/handlers/role/remove-permission.handler.ts new file mode 100644 index 0000000..6793a68 --- /dev/null +++ b/src/modules/identity/application/handlers/role/remove-permission.handler.ts @@ -0,0 +1,27 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; +import { IRole } from '../../../domain/repositories/role.interface'; +import { AuthorizationService } from '../../../../authorization/authorization.service'; +import { EventBus } from '../../../../../core/event-bus/event-bus.service'; + +@Injectable() +export class RoleRemovePermissionHandler { + constructor(private readonly roleRepository: IRole, private readonly authorizationService: AuthorizationService, private readonly events: EventBus) {} + + async execute(roleId: string, permissionId: string) { + const role = await this.roleRepository.findById(roleId); + if (!role) throw new NotFoundException('Role not found.'); + + role.removePermission(permissionId); + + const updated = await this.roleRepository.update(role); + + // Invalidate caches for users with the role + const userIds = await this.roleRepository.getAssignedUserIds(roleId); + for (const uid of userIds) await this.authorizationService.invalidateUserPermissions(uid); + + // Publish event + this.events.publish({ id: require('crypto').randomUUID(), timestamp: new Date().toISOString(), type: 'RoleUpdated', payload: { roleId, removedPermissionId: permissionId } }); + + return updated; + } +} diff --git a/src/modules/identity/application/handlers/role/update-role.handler.ts b/src/modules/identity/application/handlers/role/update-role.handler.ts new file mode 100644 index 0000000..b060cd7 --- /dev/null +++ b/src/modules/identity/application/handlers/role/update-role.handler.ts @@ -0,0 +1,31 @@ +import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common'; +import { IRole } from '../../../domain/repositories/role.interface'; +import { AuthorizationService } from '../../../../authorization/authorization.service'; +import { EventBus } from '../../../../../core/event-bus/event-bus.service'; + +@Injectable() +export class UpdateRoleHandler { + constructor(private readonly roleRepository: IRole, private readonly authorizationService: AuthorizationService, private readonly events: EventBus) {} + + async execute(id: string, dto: any) { + const role = await this.roleRepository.findById(id); + if (!role) throw new NotFoundException('Role not found.'); + + // validation + if (dto.name && dto.name.length < 2) throw new BadRequestException('Name too short'); + + if (dto.name) role.changeName(dto.name); + if (dto.description !== undefined) role.changeDescription(dto.description); + if (dto.isDefault !== undefined) role.setDefault(!!dto.isDefault); + + const updated = await this.roleRepository.update(role); + + // invalidate caches for users with this role + const userIds = await this.roleRepository.getAssignedUserIds(id); + for (const uid of userIds) await this.authorizationService.invalidateUserPermissions(uid); + + this.events.publish({ id: require('crypto').randomUUID(), timestamp: new Date().toISOString(), type: 'RoleUpdated', payload: { roleId: id } }); + + return updated; + } +} diff --git a/src/modules/identity/application/handlers/user/assign-permission.handler.ts b/src/modules/identity/application/handlers/user/assign-permission.handler.ts new file mode 100644 index 0000000..28b3b86 --- /dev/null +++ b/src/modules/identity/application/handlers/user/assign-permission.handler.ts @@ -0,0 +1,26 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; +import { IUser } from '../../../domain/repositories/user.interface'; +import { PermissionData } from '../../../domain/entities/permission.entity'; + +@Injectable() +export class AssignPermissionHandler { + constructor(private readonly userRepository: IUser) {} + + async execute(userId: string, permissionId: string) { + const user = await this.userRepository.getById(userId); + if (!user) throw new NotFoundException('User not found.'); + + // Permission repository is not available; create a minimal PermissionData + const perm = PermissionData.restore({ + id: permissionId, + name: permissionId, + description: '', + createdAt: new Date(), + updatedAt: new Date(), + }); + + user.assignPermission(perm); + + return this.userRepository.update(user); + } +} diff --git a/src/modules/identity/application/handlers/user/assign-role.handler.ts b/src/modules/identity/application/handlers/user/assign-role.handler.ts new file mode 100644 index 0000000..2b07179 --- /dev/null +++ b/src/modules/identity/application/handlers/user/assign-role.handler.ts @@ -0,0 +1,23 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; +import { IUser } from '../../../domain/repositories/user.interface'; +import { IRole } from '../../../domain/repositories/role.interface'; + +@Injectable() +export class AssignRoleHandler { + constructor( + private readonly userRepository: IUser, + private readonly roleRepository: IRole, + ) {} + + async execute(userId: string, roleId: string) { + const user = await this.userRepository.getById(userId); + if (!user) throw new NotFoundException('User not found.'); + + const role = await this.roleRepository.findById(roleId); + if (!role) throw new NotFoundException('Role not found.'); + + user.assignRole(role); + + return this.userRepository.update(user); + } +} diff --git a/src/modules/identity/application/handlers/user/create-user.handler.ts b/src/modules/identity/application/handlers/user/create-user.handler.ts new file mode 100644 index 0000000..5bee566 --- /dev/null +++ b/src/modules/identity/application/handlers/user/create-user.handler.ts @@ -0,0 +1,31 @@ +import { + Injectable, + ConflictException, + BadRequestException, +} from '@nestjs/common'; + +import { CreateUserDto } from '../../../presentation/dto/create-user.dto'; +import { IUser } from '../../../domain/repositories/user.interface'; +import { UserData } from '../../../domain/entities/user.entity'; +import { UserService } from '../../services/user.service'; + +@Injectable() +export class CreateUserHandler { + constructor( + private readonly userRepository: IUser, + private readonly userService: UserService, + ) {} + + async execute(dto: CreateUserDto): Promise { + + const exists = await this.userRepository.existByEmail(dto.email); + + if (exists) { + throw new ConflictException('Email already exists.'); + } + + // Provisioning disabled: Do not create users in external IdP from RayLab. + // Reject attempts to create users via API to enforce creation-at-Authentik policy. + throw new BadRequestException('User creation via RayLab API is disabled. Create users in Authentik and then login to sync.'); + } +} \ No newline at end of file diff --git a/src/modules/identity/application/handlers/user/delete-user.handler.ts b/src/modules/identity/application/handlers/user/delete-user.handler.ts new file mode 100644 index 0000000..17c98a4 --- /dev/null +++ b/src/modules/identity/application/handlers/user/delete-user.handler.ts @@ -0,0 +1,14 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; +import { IUser } from '../../../domain/repositories/user.interface'; + +@Injectable() +export class DeleteUserHandler { + constructor(private readonly userRepository: IUser) {} + + async execute(id: string) { + const exists = await this.userRepository.existsById(id); + if (!exists) throw new NotFoundException('User not found.'); + + await this.userRepository.softDelete(id); + } +} diff --git a/src/modules/identity/application/handlers/user/disable-user.handler.ts b/src/modules/identity/application/handlers/user/disable-user.handler.ts new file mode 100644 index 0000000..87c627a --- /dev/null +++ b/src/modules/identity/application/handlers/user/disable-user.handler.ts @@ -0,0 +1,14 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; +import { IUser } from '../../../domain/repositories/user.interface'; + +@Injectable() +export class DisableUserHandler { + constructor(private readonly userRepository: IUser) {} + + async execute(id: string) { + const exists = await this.userRepository.existsById(id); + if (!exists) throw new NotFoundException('User not found.'); + + return this.userRepository.disable(id); + } +} diff --git a/src/modules/identity/application/handlers/user/enable-user.handler.ts b/src/modules/identity/application/handlers/user/enable-user.handler.ts new file mode 100644 index 0000000..dd73fbd --- /dev/null +++ b/src/modules/identity/application/handlers/user/enable-user.handler.ts @@ -0,0 +1,14 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; +import { IUser } from '../../../domain/repositories/user.interface'; + +@Injectable() +export class EnableUserHandler { + constructor(private readonly userRepository: IUser) {} + + async execute(id: string) { + const exists = await this.userRepository.existsById(id); + if (!exists) throw new NotFoundException('User not found.'); + + return this.userRepository.enable(id); + } +} diff --git a/src/modules/identity/application/handlers/user/get-current-user.handler.ts b/src/modules/identity/application/handlers/user/get-current-user.handler.ts new file mode 100644 index 0000000..a9e2184 --- /dev/null +++ b/src/modules/identity/application/handlers/user/get-current-user.handler.ts @@ -0,0 +1,10 @@ +import { Injectable } from '@nestjs/common'; +import { UserData } from '../../../domain/entities/user.entity'; + +@Injectable() +export class GetCurrentUserHandler { + async execute(currentUser: UserData) { + // currentUser is already domain user attached by CurrentUserGuard + return currentUser; + } +} diff --git a/src/modules/identity/application/handlers/user/get-user.handler.ts b/src/modules/identity/application/handlers/user/get-user.handler.ts new file mode 100644 index 0000000..d78d600 --- /dev/null +++ b/src/modules/identity/application/handlers/user/get-user.handler.ts @@ -0,0 +1,11 @@ +import { Injectable } from '@nestjs/common'; +import { IUser } from '../../../domain/repositories/user.interface'; + +@Injectable() +export class GetUserHandler { + constructor(private readonly userRepository: IUser) {} + + async execute(id: string) { + return this.userRepository.getById(id); + } +} diff --git a/src/modules/identity/application/handlers/user/get-users.handler.ts b/src/modules/identity/application/handlers/user/get-users.handler.ts new file mode 100644 index 0000000..fe3af46 --- /dev/null +++ b/src/modules/identity/application/handlers/user/get-users.handler.ts @@ -0,0 +1,27 @@ +import { Injectable } from '@nestjs/common'; +import { IUser } from '../../../domain/repositories/user.interface'; + +@Injectable() +export class GetUsersHandler { + constructor(private readonly userRepository: IUser) {} + + async execute(query: { + page?: number; + limit?: number; + search?: string; + isActive?: boolean; + deleted?: boolean; + roleId?: string; + }) { + const res = await this.userRepository.find({ + page: query.page, + limit: query.limit, + search: query.search || null, + isActive: query.isActive !== undefined ? query.isActive : null, + deleted: query.deleted !== undefined ? query.deleted : null, + roleId: query.roleId || null, + }); + + return res; + } +} diff --git a/src/modules/identity/application/handlers/user/remove-permission.handler.ts b/src/modules/identity/application/handlers/user/remove-permission.handler.ts new file mode 100644 index 0000000..c8cb40d --- /dev/null +++ b/src/modules/identity/application/handlers/user/remove-permission.handler.ts @@ -0,0 +1,16 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; +import { IUser } from '../../../domain/repositories/user.interface'; + +@Injectable() +export class RemovePermissionHandler { + constructor(private readonly userRepository: IUser) {} + + async execute(userId: string, permissionId: string) { + const user = await this.userRepository.getById(userId); + if (!user) throw new NotFoundException('User not found.'); + + user.removePermission(permissionId); + + return this.userRepository.update(user); + } +} diff --git a/src/modules/identity/application/handlers/user/remove-role.handler.ts b/src/modules/identity/application/handlers/user/remove-role.handler.ts new file mode 100644 index 0000000..b6c855a --- /dev/null +++ b/src/modules/identity/application/handlers/user/remove-role.handler.ts @@ -0,0 +1,16 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; +import { IUser } from '../../../domain/repositories/user.interface'; + +@Injectable() +export class RemoveRoleHandler { + constructor(private readonly userRepository: IUser) {} + + async execute(userId: string, roleId: string) { + const user = await this.userRepository.getById(userId); + if (!user) throw new NotFoundException('User not found.'); + + user.removeRole(roleId); + + return this.userRepository.update(user); + } +} diff --git a/src/modules/identity/application/handlers/user/restore-user.handler.ts b/src/modules/identity/application/handlers/user/restore-user.handler.ts new file mode 100644 index 0000000..657ee47 --- /dev/null +++ b/src/modules/identity/application/handlers/user/restore-user.handler.ts @@ -0,0 +1,14 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; +import { IUser } from '../../../domain/repositories/user.interface'; + +@Injectable() +export class RestoreUserHandler { + constructor(private readonly userRepository: IUser) {} + + async execute(id: string) { + const exists = await this.userRepository.existsById(id); + if (!exists) throw new NotFoundException('User not found.'); + + return this.userRepository.restore(id); + } +} diff --git a/src/modules/identity/application/handlers/user/sync-identity.handler.ts b/src/modules/identity/application/handlers/user/sync-identity.handler.ts new file mode 100644 index 0000000..aeb114b --- /dev/null +++ b/src/modules/identity/application/handlers/user/sync-identity.handler.ts @@ -0,0 +1,86 @@ +import { Injectable, UnauthorizedException, ForbiddenException, Inject } from '@nestjs/common'; +import { IUser } from '../../../domain/repositories/user.interface'; +import { IRole } from '../../../domain/repositories/role.interface'; +import { IAuthConfig } from '../../config/i-auth-config'; +import { UserData } from '../../../domain/entities/user.entity'; +import { IdentityData } from '../../../../../core/auth/interfaces/identity-data'; + +@Injectable() +export class SyncIdentityHandler { + constructor( + private readonly userRepository: IUser, + private readonly roleRepository: IRole, + private readonly authConfig: IAuthConfig, + ) {} + + async execute(identity: IdentityData): Promise { + const { sub, preferred_username, email } = identity as any; + + if (!sub) throw new UnauthorizedException('Invalid identity payload.'); + + // Try find by authentikId + let user = await this.userRepository.findByAuthentikId(sub); + + const syncEmail = this.authConfig.syncEmail(); + const syncUsername = this.authConfig.syncUsername(); + + if (!user) { + // User does not exist locally: create minimal local record per new architecture + const username = preferred_username || email || sub; + const userEntity = UserData.create({ username, email: email || '', authentikId: sub }); + + // assign default role if available + try { + const defaultRole = await this.roleRepository.getDefaultRole(); + if (defaultRole) { + userEntity.assignRole(defaultRole); + } + } catch (e) { + // ignore if role repo not available or no default role + } + + // persist local user + user = await this.userRepository.create(userEntity); + + // return freshly created user + return user; + } else { + let changed = false; + + if (syncEmail && email && user.email !== email) { + user.changeEmail(email); + changed = true; + + // prepare event class instance if needed (no dispatch) + } + + if (syncUsername && preferred_username && user.username !== preferred_username) { + user.changeUsername(preferred_username); + changed = true; + + // prepare event class instance if needed (no dispatch) + } + + // update last seen + user.touchLastSeen(); + changed = true; + + if (changed) { + user = await this.userRepository.update(user); + } + } + + // Validations + if (!user.isActive) { + // Authentication succeeded but user is disabled + throw new ForbiddenException('User is not active.'); + } + + if (user.deletedAt) { + throw new ForbiddenException('User is deleted.'); + } + + // Ensure roles/permissions loaded (repo should return includes) + return user; + } +} diff --git a/src/modules/identity/application/handlers/user/update-user.handler.ts b/src/modules/identity/application/handlers/user/update-user.handler.ts new file mode 100644 index 0000000..21c8156 --- /dev/null +++ b/src/modules/identity/application/handlers/user/update-user.handler.ts @@ -0,0 +1,25 @@ +import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common'; +import { IUser } from '../../../domain/repositories/user.interface'; +import { UpdateUserDto } from '../../../presentation/dto/update-user.dto'; + +@Injectable() +export class UpdateUserHandler { + constructor(private readonly userRepository: IUser) {} + + async execute(id: string, dto: UpdateUserDto) { + // Password management is not allowed in RayLab Core + if ((dto as any).password) throw new BadRequestException('Password management is not allowed.'); + + const user = await this.userRepository.getById(id); + if (!user) throw new NotFoundException('User not found.'); + + // Only allow updating profile fields: name, email, metadata, storage settings + if (dto.name !== undefined) user.changeUsername(dto.name); + if (dto.email !== undefined) user.changeEmail(dto.email); + // metadata field not handled at domain level yet + if ((dto as any).storageQuota !== undefined) user.setStorageQuota((dto as any).storageQuota); + if ((dto as any).storageUsed !== undefined) user.setStorageUsed((dto as any).storageUsed); + + return this.userRepository.update(user); + } +} diff --git a/src/modules/identity/application/services/README.md b/src/modules/identity/application/services/README.md deleted file mode 100644 index 3ac5650..0000000 --- a/src/modules/identity/application/services/README.md +++ /dev/null @@ -1,12 +0,0 @@ -services/ - -Penjelasan: -Application services (use-cases) untuk module identity. - -Contoh file: -- get-user.service.ts -- create-user.service.ts - -Aturan: -- Application service mengorkestrasi domain services dan repository. -- Menangani transaction boundary jika perlu. diff --git a/src/modules/identity/application/services/user.service.ts b/src/modules/identity/application/services/user.service.ts new file mode 100644 index 0000000..bd921df --- /dev/null +++ b/src/modules/identity/application/services/user.service.ts @@ -0,0 +1,20 @@ +import { Injectable, BadRequestException, Logger } from '@nestjs/common'; +import { PrismaService } from '../../../../shared/prisma.service'; +import { IUser } from '../../domain/repositories/user.interface'; +import { UserData } from '../../domain/entities/user.entity'; + +@Injectable() +export class UserService { + private readonly logger = new Logger(UserService.name); + + constructor( + private readonly prisma: PrismaService, + private readonly userRepository: IUser, + ) {} + + async createUser(input: { username: string; email: string; roles?: string[]; isActive?: boolean }) { + // Provisioning to external Identity Provider (Authentik) has been disabled. + // All users must be created in Authentik first. RayLab will create local record on first successful login. + throw new BadRequestException('Provisioning disabled: create users in Authentik and login to sync to RayLab'); + } +} diff --git a/src/modules/identity/domain/entities/README.md b/src/modules/identity/domain/entities/README.md index bf9110b..b394cb0 100644 --- a/src/modules/identity/domain/entities/README.md +++ b/src/modules/identity/domain/entities/README.md @@ -1,11 +1 @@ -entities/ - -Penjelasan: -Entity domain untuk identity, mis. User, Profile. - -Contoh file: -- user.entity.ts -- profile.entity.ts - -Aturan: -- Entity berisi atribut dan mungkin method domain kecil (invariants), bukan orchestration. +ini adalah tempat class objek dibuat, objek user, role, dll. disini juga ada method dan harus diingat yang boleh mengubah objek ini hanya objek ini sendiri. \ No newline at end of file diff --git a/src/modules/identity/domain/entities/permission.entity.ts b/src/modules/identity/domain/entities/permission.entity.ts new file mode 100644 index 0000000..f27dce1 --- /dev/null +++ b/src/modules/identity/domain/entities/permission.entity.ts @@ -0,0 +1,40 @@ +export class PermissionData { + private constructor( + public readonly id: string, + public code: string, + public name: string, + public description: string, + public createdAt: Date, + public updatedAt: Date, + ) {} + + changeName(name: string) { + this.name = name; + } + + changeDescription(description: string) { + this.description = description; + } + + changeCode(code: string) { + this.code = code; + } + + static restore(props: { + id: string; + code?: string; + name: string; + description: string; + createdAt: Date; + updatedAt: Date; + }) { + return new PermissionData( + props.id, + props.code || props.name, + props.name, + props.description, + props.createdAt, + props.updatedAt, + ); + } +} \ No newline at end of file diff --git a/src/modules/identity/domain/entities/role.entity.ts b/src/modules/identity/domain/entities/role.entity.ts new file mode 100644 index 0000000..96a0d0f --- /dev/null +++ b/src/modules/identity/domain/entities/role.entity.ts @@ -0,0 +1,64 @@ +import { PermissionData } from "./permission.entity"; + +export class RoleData { + private constructor( + public readonly id: string, + public code: string, + public name: string, + public description: string, + public permissions: PermissionData[], + public isDefault: boolean, + public createdAt: Date, + public updatedAt: Date, + ) {} + + hasPermissions(permissionId: string): boolean { + return this.permissions.some(x => x.id.toLowerCase() === permissionId.toLowerCase()); + } + + assignPermission(permissionData: PermissionData): void { + if (this.hasPermissions(permissionData.id)) throw new Error("Permission sudah dimiliki."); + + this.permissions.push(permissionData); + } + + removePermission(permissionId: string): void { + const idx = this.permissions.findIndex(p => p.id.toLowerCase() === permissionId.toLowerCase()); + if (idx === -1) throw new Error('Permission tidak ditemukan pada role.'); + this.permissions.splice(idx, 1); + } + + changeName(name: string) { + this.name = name; + } + + changeDescription(description: string) { + this.description = description; + } + + setDefault(isDefault: boolean) { + this.isDefault = isDefault; + } + + static restore(props: { + id: string; + code: string; + name: string; + description: string; + permissions?: PermissionData[]; + isDefault?: boolean; + createdAt: Date; + updatedAt: Date; + }) { + return new RoleData( + props.id, + props.code, + props.name, + props.description, + props.permissions || [], + props.isDefault ?? false, + props.createdAt, + props.updatedAt, + ); + } +} \ No newline at end of file diff --git a/src/modules/identity/domain/events/user-created.event.ts b/src/modules/identity/domain/events/user-created.event.ts index 07fea7e..997431e 100644 --- a/src/modules/identity/domain/events/user-created.event.ts +++ b/src/modules/identity/domain/events/user-created.event.ts @@ -2,5 +2,6 @@ import { DomainEvent } from '../../../../core/events/event.interface'; export class UserCreatedEvent implements DomainEvent { readonly name = 'UserCreated'; - constructor(public readonly payload: any, public readonly occurredAt: Date = new Date()) {} + readonly occurredAt: Date = new Date(); + constructor(public readonly payload: { userId: string; authentikId: string; email: string; username: string }) {} } diff --git a/src/modules/identity/domain/events/user-deleted.event.ts b/src/modules/identity/domain/events/user-deleted.event.ts deleted file mode 100644 index 07cc1d4..0000000 --- a/src/modules/identity/domain/events/user-deleted.event.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { DomainEvent } from '../../../../core/events/event.interface'; - -export class UserDeletedEvent implements DomainEvent { - readonly name = 'UserDeleted'; - constructor(public readonly payload: any, public readonly occurredAt: Date = new Date()) {} -} diff --git a/src/modules/identity/domain/events/user-disabled.event.ts b/src/modules/identity/domain/events/user-disabled.event.ts new file mode 100644 index 0000000..2f243fc --- /dev/null +++ b/src/modules/identity/domain/events/user-disabled.event.ts @@ -0,0 +1,7 @@ +import { DomainEvent } from '../../../../core/events/event.interface'; + +export class UserDisabledEvent implements DomainEvent { + readonly name = 'UserDisabled'; + readonly occurredAt: Date = new Date(); + constructor(public readonly payload: { userId: string }) {} +} diff --git a/src/modules/identity/domain/events/user-email-changed.event.ts b/src/modules/identity/domain/events/user-email-changed.event.ts new file mode 100644 index 0000000..db50cbc --- /dev/null +++ b/src/modules/identity/domain/events/user-email-changed.event.ts @@ -0,0 +1,7 @@ +import { DomainEvent } from '../../../../core/events/event.interface'; + +export class UserEmailChangedEvent implements DomainEvent { + readonly name = 'UserEmailChanged'; + readonly occurredAt: Date = new Date(); + constructor(public readonly payload: { userId: string; oldEmail: string; newEmail: string }) {} +} diff --git a/src/modules/identity/domain/events/user-enabled.event.ts b/src/modules/identity/domain/events/user-enabled.event.ts new file mode 100644 index 0000000..9ed1c60 --- /dev/null +++ b/src/modules/identity/domain/events/user-enabled.event.ts @@ -0,0 +1,7 @@ +import { DomainEvent } from '../../../../core/events/event.interface'; + +export class UserEnabledEvent implements DomainEvent { + readonly name = 'UserEnabled'; + readonly occurredAt: Date = new Date(); + constructor(public readonly payload: { userId: string }) {} +} diff --git a/src/modules/identity/domain/events/user-username-changed.event.ts b/src/modules/identity/domain/events/user-username-changed.event.ts new file mode 100644 index 0000000..7023331 --- /dev/null +++ b/src/modules/identity/domain/events/user-username-changed.event.ts @@ -0,0 +1,7 @@ +import { DomainEvent } from '../../../../core/events/event.interface'; + +export class UserUsernameChangedEvent implements DomainEvent { + readonly name = 'UserUsernameChanged'; + readonly occurredAt: Date = new Date(); + constructor(public readonly payload: { userId: string; oldUsername: string; newUsername: string }) {} +} diff --git a/src/modules/identity/domain/repositories/README.MD b/src/modules/identity/domain/repositories/README.MD new file mode 100644 index 0000000..1d0ee81 --- /dev/null +++ b/src/modules/identity/domain/repositories/README.MD @@ -0,0 +1 @@ +ini adalah interface, semua yang akan dibuat oleh application/handler harus dibuat disini dulu. \ No newline at end of file diff --git a/src/modules/identity/domain/repositories/permission.interface.ts b/src/modules/identity/domain/repositories/permission.interface.ts new file mode 100644 index 0000000..d17f078 --- /dev/null +++ b/src/modules/identity/domain/repositories/permission.interface.ts @@ -0,0 +1,12 @@ +import { PermissionData } from '../entities/permission.entity'; + +export abstract class IPermission { + abstract find(params: { page?: number; limit?: number; search?: string | null }): Promise<{ data: PermissionData[]; total: number }>; + abstract getById(id: string): Promise; + abstract create(permission: PermissionData): Promise; + abstract update(permission: PermissionData): Promise; + abstract delete(id: string): Promise; + + // Returns role ids that reference this permission + abstract findRoleIdsByPermission(permissionId: string): Promise; +} diff --git a/src/modules/identity/domain/repositories/role.interface.ts b/src/modules/identity/domain/repositories/role.interface.ts new file mode 100644 index 0000000..9031700 --- /dev/null +++ b/src/modules/identity/domain/repositories/role.interface.ts @@ -0,0 +1,15 @@ +import { RoleData } from '../entities/role.entity'; + +export abstract class IRole { + abstract getDefaultRole(): Promise; + abstract findById(roleId: string): Promise; + + abstract find(params: { page?: number; limit?: number; search?: string | null }): Promise<{ data: RoleData[]; total: number }>; + + abstract create(role: RoleData): Promise; + abstract update(role: RoleData): Promise; + abstract delete(roleId: string): Promise; + + // Returns user ids assigned to a role (preserves layering) + abstract getAssignedUserIds(roleId: string): Promise; +} diff --git a/src/modules/identity/domain/repositories/user.interface.ts b/src/modules/identity/domain/repositories/user.interface.ts new file mode 100644 index 0000000..7a34862 --- /dev/null +++ b/src/modules/identity/domain/repositories/user.interface.ts @@ -0,0 +1,29 @@ +import { UserData } from '../entities/user.entity'; + +export abstract class IUser { + abstract create(user: UserData): Promise; + + abstract existsById(userId: string): Promise; + abstract existByEmail(userEmail: string): Promise; + abstract getById(userId: string): Promise; + abstract getByEmail(userEmail: string): Promise; + + abstract findByAuthentikId(authentikId: string): Promise; + + abstract update(user: UserData): Promise; + + abstract find(params: { + page?: number; + limit?: number; + search?: string | null; + isActive?: boolean | null; + deleted?: boolean | null; + roleId?: string | null; + }): Promise<{ data: UserData[]; total: number }>; + + abstract softDelete(userId: string): Promise; + abstract restore(userId: string): Promise; + + abstract enable(userId: string): Promise; + abstract disable(userId: string): Promise; +} \ No newline at end of file diff --git a/src/modules/identity/infrastructure/README.md b/src/modules/identity/infrastructure/README.md index 06bd408..006593d 100644 --- a/src/modules/identity/infrastructure/README.md +++ b/src/modules/identity/infrastructure/README.md @@ -1,12 +1 @@ -modules/identity/infrastructure/ - -Penjelasan: -Implementasi teknis untuk module identity, seperti Prisma repository, adapter implementations, dan data mappers. - -Contoh file: -- prisma/user.repository.ts (mengimplementasikan domain repository interface) -- adapter/identity-adapter.ts - -Aturan: -- Infrastruktur hanya mengimplementasikan interface domain; jangan memuat business rules. -- Import dari infrastructure ke domain harus satu arah: infrastructure -> domain (implementasi). +infrastrucute adalah tempat yang langsung berhubungan dengan dunia luar, misal db, server lain, JWT, dsb. \ No newline at end of file diff --git a/src/modules/identity/infrastructure/mappers/.gitkeep b/src/modules/identity/infrastructure/mappers/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/modules/identity/infrastructure/mappers/README.MD b/src/modules/identity/infrastructure/mappers/README.MD new file mode 100644 index 0000000..7a42625 --- /dev/null +++ b/src/modules/identity/infrastructure/mappers/README.MD @@ -0,0 +1 @@ +disini emngubah dari domain/entity menjadi format di database dan sebaliknya. \ No newline at end of file diff --git a/src/modules/identity/infrastructure/mappers/prisma-permission.mapper.ts b/src/modules/identity/infrastructure/mappers/prisma-permission.mapper.ts new file mode 100644 index 0000000..05abad0 --- /dev/null +++ b/src/modules/identity/infrastructure/mappers/prisma-permission.mapper.ts @@ -0,0 +1,23 @@ +import { PermissionData } from '../../domain/entities/permission.entity'; + +export class PrismaPermissionMapper { + static toDomain(model: any): PermissionData { + return PermissionData.restore({ + id: model.id, + code: model.code || model.name, + name: model.name, + description: model.description, + createdAt: model.createdAt, + updatedAt: model.updatedAt, + }); + } + + static toPersistence(permission: PermissionData) { + return { + id: permission.id, + code: permission.code, + name: permission.name, + description: permission.description, + }; + } +} diff --git a/src/modules/identity/infrastructure/mappers/prisma-role.mapper.ts b/src/modules/identity/infrastructure/mappers/prisma-role.mapper.ts new file mode 100644 index 0000000..71ecf55 --- /dev/null +++ b/src/modules/identity/infrastructure/mappers/prisma-role.mapper.ts @@ -0,0 +1,28 @@ +import { RoleData } from '../../domain/entities/role.entity'; +import { PermissionData } from '../../domain/entities/permission.entity'; + +export class PrismaRoleMapper { + static toDomain(model: any): RoleData { + const permissions: PermissionData[] = (model.permissions || []).map((p: any) => { + const perm = p.permission || p; // p may be RolePermission with nested permission or permission itself + return PermissionData.restore({ + id: perm.id, + name: perm.name, + description: perm.description, + createdAt: perm.createdAt, + updatedAt: perm.updatedAt, + }); + }); + + return RoleData.restore({ + id: model.id, + code: model.code, + name: model.name, + description: model.description, + permissions, + isDefault: model.isDefault ?? false, + createdAt: model.createdAt, + updatedAt: model.updatedAt, + }); + } +} \ No newline at end of file diff --git a/src/modules/identity/infrastructure/mappers/prisma-user.mapper.ts b/src/modules/identity/infrastructure/mappers/prisma-user.mapper.ts index 1ce360d..6c3cbc0 100644 --- a/src/modules/identity/infrastructure/mappers/prisma-user.mapper.ts +++ b/src/modules/identity/infrastructure/mappers/prisma-user.mapper.ts @@ -1,27 +1,71 @@ -import { User } from '../../domain/entities/user.entity'; +import { UserData } from '../../domain/entities/user.entity'; +import { RoleData } from '../../domain/entities/role.entity'; +import { PermissionData } from '../../domain/entities/permission.entity'; export class PrismaUserMapper { - static toDomain(model: any): User | null { - if (!model) { - return null; - } + static toDomain(model: any): UserData { + const roles: RoleData[] = (model.roles || []).map((ur: any) => { + const r = ur.role; + const permissions: PermissionData[] = (r?.permissions || []).map((rp: any) => + PermissionData.restore({ + id: rp.permission.id, + name: rp.permission.name, + description: rp.permission.description, + createdAt: rp.permission.createdAt, + updatedAt: rp.permission.updatedAt, + }), + ); - return User.restore({ + return RoleData.restore({ + id: r.id, + code: r.code, + name: r.name, + description: r.description, + permissions, + createdAt: r.createdAt, + updatedAt: r.updatedAt, + }); + }); + + const permissions: PermissionData[] = (model.permissions || []).map((up: any) => + PermissionData.restore({ + id: up.permission.id, + name: up.permission.name, + description: up.permission.description, + createdAt: up.permission.createdAt, + updatedAt: up.permission.updatedAt, + }), + ); + + return UserData.restore({ id: model.id, - name: model.name, - email: model.email, - password: model.password, - metadata: model.metadata, + authentikId: model.authentikUserId || model.authentikId || null, + username: model.username || model.name || model.username, + email: model.email, + roles, + permissions, + isActive: model.isActive ?? true, + deletedAt: model.deletedAt || null, + lastSeenAt: model.lastSeenAt || null, + storageQuota: model.storageQuota !== undefined && model.storageQuota !== null ? Number(model.storageQuota) : undefined, + storageUsed: model.storageUsed !== undefined && model.storageUsed !== null ? Number(model.storageUsed) : undefined, }); } - static toPersistence(user: User) { - return { - id: user.id, - name: user.name, - email: user.email, - password: user.password, - metadata: user.metadata ?? {}, - }; + static toPersistence(userData: UserData) { + return { + id: userData.id, + authentikUserId: (userData as any).authentikUserId || userData.authentikId, + authentikId: userData.authentikId, + username: userData.username, + email: userData.email, + isActive: userData.isActive, + deletedAt: userData.deletedAt, + lastSeenAt: userData.lastSeenAt, + lastSyncedAt: (userData as any).lastSyncedAt, + syncStatus: (userData as any).syncStatus, + storageQuota: (userData as any).storageQuota, + storageUsed: (userData as any).storageUsed, + }; } } \ No newline at end of file diff --git a/src/modules/identity/infrastructure/persistence/README.md b/src/modules/identity/infrastructure/persistence/README.md new file mode 100644 index 0000000..07d7361 --- /dev/null +++ b/src/modules/identity/infrastructure/persistence/README.md @@ -0,0 +1 @@ +ini adalah tempat konfigurasi database. \ No newline at end of file diff --git a/src/modules/identity/infrastructure/repositories/README.md b/src/modules/identity/infrastructure/repositories/README.md index b7e780b..455ca20 100644 --- a/src/modules/identity/infrastructure/repositories/README.md +++ b/src/modules/identity/infrastructure/repositories/README.md @@ -1,11 +1 @@ -repositories/ - -Penjelasan: -Implementasi repository di layer infrastructure. Biasanya berisi Prisma queries dan mapping antara DB model dan domain entity. - -Contoh file: -- prisma/user.repository.ts - -Aturan: -- Repository mengimplementasikan interface di domain layer. -- Hindari business logic di repository. +disini yang menjalankan logika bisnisnya. \ No newline at end of file diff --git a/src/modules/identity/infrastructure/repositories/prisma-permission.repository.ts b/src/modules/identity/infrastructure/repositories/prisma-permission.repository.ts new file mode 100644 index 0000000..a1a3f4b --- /dev/null +++ b/src/modules/identity/infrastructure/repositories/prisma-permission.repository.ts @@ -0,0 +1,59 @@ +import { Injectable } from '@nestjs/common'; +import { PrismaService } from '../../../../shared/prisma.service'; +import { IPermission } from '../../domain/repositories/permission.interface'; +import { PrismaPermissionMapper } from '../mappers/prisma-permission.mapper'; +import { PermissionData } from '../../domain/entities/permission.entity'; + +@Injectable() +export class PrismaPermissionRepository implements IPermission { + constructor(private readonly prisma: PrismaService) {} + + async find(params: { page?: number; limit?: number; search?: string | null }) { + const page = params.page && params.page > 0 ? params.page : 1; + const limit = params.limit && params.limit > 0 ? params.limit : 10; + + const where: any = {}; + if (params.search) { + where.OR = [ + { name: { contains: params.search, mode: 'insensitive' } }, + { code: { contains: params.search, mode: 'insensitive' } }, + { description: { contains: params.search, mode: 'insensitive' } }, + ]; + } + + const [total, items] = await Promise.all([ + this.prisma.permission.count({ where }), + this.prisma.permission.findMany({ where, skip: (page - 1) * limit, take: limit, orderBy: { createdAt: 'desc' } }), + ]); + + return { data: items.map(i => PrismaPermissionMapper.toDomain(i)!), total }; + } + + async getById(id: string) { + const p = await this.prisma.permission.findUnique({ where: { id } }); + if (!p) throw new Error('Permission not found.'); + return PrismaPermissionMapper.toDomain(p); + } + + async create(permission: PermissionData) { + const base = PrismaPermissionMapper.toPersistence(permission); + const created = await this.prisma.permission.create({ data: base }); + return PrismaPermissionMapper.toDomain(created)!; + } + + async update(permission: PermissionData) { + const base = PrismaPermissionMapper.toPersistence(permission); + const updated = await this.prisma.permission.update({ where: { id: permission.id }, data: base }); + return PrismaPermissionMapper.toDomain(updated)!; + } + + async delete(id: string) { + await this.prisma.permission.delete({ where: { id } }); + } + + async findRoleIdsByPermission(permissionId: string): Promise { + const rows = await this.prisma.rolePermission.findMany({ where: { permissionId }, select: { roleId: true } }); + return rows.map(r => r.roleId); + } +} + diff --git a/src/modules/identity/infrastructure/repositories/prisma-role.repository.ts b/src/modules/identity/infrastructure/repositories/prisma-role.repository.ts new file mode 100644 index 0000000..8d0f28e --- /dev/null +++ b/src/modules/identity/infrastructure/repositories/prisma-role.repository.ts @@ -0,0 +1,115 @@ +import { Injectable } from '@nestjs/common'; +import { PrismaService } from '../../../../shared/prisma.service'; +import { IRole } from '../../domain/repositories/role.interface'; +import { PrismaRoleMapper } from '../mappers/prisma-role.mapper'; +import { RoleData } from '../../domain/entities/role.entity'; + +@Injectable() +export class PrismaRoleRepository implements IRole { + constructor(private readonly prisma: PrismaService) {} + + async getDefaultRole() { + const role = await this.prisma.role.findFirst({ + where: { isDefault: true }, + include: { permissions: { include: { permission: true } } }, + }); + + if (!role) return null; + + return PrismaRoleMapper.toDomain(role); + } + + async findById(roleId: string) { + const role = await this.prisma.role.findUnique({ + where: { id: roleId }, + include: { permissions: { include: { permission: true } } }, + }); + + if (!role) throw new Error('Role not found.'); + + return PrismaRoleMapper.toDomain(role); + } + + async find(params: { page?: number; limit?: number; search?: string | null }) { + const page = params.page && params.page > 0 ? params.page : 1; + const limit = params.limit && params.limit > 0 ? params.limit : 10; + + const where: any = {}; + if (params.search) { + where.OR = [ + { name: { contains: params.search, mode: 'insensitive' } }, + { code: { contains: params.search, mode: 'insensitive' } }, + { description: { contains: params.search, mode: 'insensitive' } }, + ]; + } + + const [total, items] = await Promise.all([ + this.prisma.role.count({ where }), + this.prisma.role.findMany({ where, skip: (page - 1) * limit, take: limit, orderBy: { createdAt: 'desc' }, include: { permissions: { include: { permission: true } } } }), + ]); + + return { data: items.map(i => PrismaRoleMapper.toDomain(i)!), total }; + } + + async create(role: RoleData) { + const base: any = { + id: role.id, + code: role.code, + name: role.name, + description: role.description, + isDefault: role.isDefault, + }; + + if (role.permissions && role.permissions.length > 0) { + base.permissions = { + create: role.permissions.map(p => ({ permission: { connect: { id: p.id } }, assignedBy: 'system' })), + }; + } + + const created = await this.prisma.role.create({ data: base, include: { permissions: { include: { permission: true } } } }); + return PrismaRoleMapper.toDomain(created)!; + } + + async update(role: RoleData) { + const base: any = { + code: role.code, + name: role.name, + description: role.description, + isDefault: role.isDefault, + }; + + // sync permissions via transaction + const permIds = role.permissions ? role.permissions.map(p => p.id) : []; + + await this.prisma.$transaction(async (prisma) => { + const currentPerms = await prisma.rolePermission.findMany({ where: { roleId: role.id } }); + const currentIds = currentPerms.map(p => p.permissionId); + + const toAdd = permIds.filter(id => !currentIds.includes(id)); + const toRemove = currentIds.filter(id => !permIds.includes(id)); + + if (toRemove.length > 0) { + await prisma.rolePermission.deleteMany({ where: { roleId: role.id, permissionId: { in: toRemove } } }); + } + + if (toAdd.length > 0) { + await prisma.rolePermission.createMany({ data: toAdd.map(pid => ({ roleId: role.id, permissionId: pid, assignedBy: 'system' })) as any, skipDuplicates: true }); + } + + await prisma.role.update({ where: { id: role.id }, data: base }); + }); + + const updated = await this.prisma.role.findUnique({ where: { id: role.id }, include: { permissions: { include: { permission: true } } } }); + return PrismaRoleMapper.toDomain(updated)!; + } + + async delete(roleId: string) { + await this.prisma.role.delete({ where: { id: roleId } }); + } + + async getAssignedUserIds(roleId: string): Promise { + const rows = await this.prisma.userRole.findMany({ where: { roleId }, select: { userId: true } }); + return rows.map(r => r.userId); + } +} + diff --git a/src/modules/identity/presentation/controllers/permissions.controller.ts b/src/modules/identity/presentation/controllers/permissions.controller.ts new file mode 100644 index 0000000..73e3fe9 --- /dev/null +++ b/src/modules/identity/presentation/controllers/permissions.controller.ts @@ -0,0 +1,67 @@ +import { Controller, Get, Post, Patch, Delete, Param, UseGuards, Body, Query } from '@nestjs/common'; +import { ApiOperation, ApiTags } from '@nestjs/swagger'; + +import { JwtAuthGuard } from '../../../../core/auth/guards/jwt-auth.guard'; +import { PermissionGuard } from '../../../authorization/presentation/guards/permission.guard'; +import { CurrentUserGuard } from '../guards/current-user.guard'; +import { Permissions } from '../../../authorization/presentation/decorators/permission.decorator'; +import { PermissionType } from '../../../../common/constants/permission.constants'; + +import { GetPermissionsHandler } from '../../application/handlers/permission/get-permissions.handler'; +import { GetPermissionHandler } from '../../application/handlers/permission/get-permission.handler'; +import { CreatePermissionHandler } from '../../application/handlers/permission/create-permission.handler'; +import { UpdatePermissionHandler } from '../../application/handlers/permission/update-permission.handler'; +import { DeletePermissionHandler } from '../../application/handlers/permission/delete-permission.handler'; + +@ApiTags('Permissions') +@Controller('permissions') +@UseGuards(JwtAuthGuard, CurrentUserGuard, PermissionGuard) +export class PermissionsController { + constructor( + private readonly getPermissionsHandler: GetPermissionsHandler, + private readonly getPermissionHandler: GetPermissionHandler, + private readonly createPermissionHandler: CreatePermissionHandler, + private readonly updatePermissionHandler: UpdatePermissionHandler, + private readonly deletePermissionHandler: DeletePermissionHandler, + ) {} + + @Get() + @Permissions(PermissionType.PERMISSION_READ) + @ApiOperation({ summary: 'List permissions' }) + async findAll(@Query() query: any) { + const res = await this.getPermissionsHandler.execute(query); + return { success: true, data: res.data, meta: { total: res.total } }; + } + + @Get(':id') + @Permissions(PermissionType.PERMISSION_READ) + @ApiOperation({ summary: 'Get permission' }) + async findOne(@Param('id') id: string) { + const p = await this.getPermissionHandler.execute(id); + return { success: true, data: p, meta: {} }; + } + + @Post() + @Permissions(PermissionType.PERMISSION_CREATE) + @ApiOperation({ summary: 'Create permission' }) + async create(@Body() body: any) { + const created = await this.createPermissionHandler.execute(body); + return { success: true, data: created, meta: {} }; + } + + @Patch(':id') + @Permissions(PermissionType.PERMISSION_UPDATE) + @ApiOperation({ summary: 'Update permission' }) + async update(@Param('id') id: string, @Body() body: any) { + const updated = await this.updatePermissionHandler.execute(id, body); + return { success: true, data: updated, meta: {} }; + } + + @Delete(':id') + @Permissions(PermissionType.PERMISSION_DELETE) + @ApiOperation({ summary: 'Delete permission' }) + async remove(@Param('id') id: string) { + await this.deletePermissionHandler.execute(id); + return { success: true, data: null, meta: {} }; + } +} diff --git a/src/modules/identity/presentation/controllers/roles.controller.ts b/src/modules/identity/presentation/controllers/roles.controller.ts new file mode 100644 index 0000000..6a8d23e --- /dev/null +++ b/src/modules/identity/presentation/controllers/roles.controller.ts @@ -0,0 +1,88 @@ +import { Controller, Get, Post, Patch, Delete, Param, UseGuards, Body, Query } from '@nestjs/common'; +import { ApiOperation, ApiTags } from '@nestjs/swagger'; + +import { JwtAuthGuard } from '../../../../core/auth/guards/jwt-auth.guard'; +import { PermissionGuard } from '../../../authorization/presentation/guards/permission.guard'; +import { CurrentUserGuard } from '../guards/current-user.guard'; +import { Permissions } from '../../../authorization/presentation/decorators/permission.decorator'; +import { PermissionType } from '../../../../common/constants/permission.constants'; + +import { GetRolesHandler } from '../../application/handlers/role/get-roles.handler'; +import { GetRoleHandler } from '../../application/handlers/role/get-role.handler'; +import { CreateRoleHandler } from '../../application/handlers/role/create-role.handler'; +import { UpdateRoleHandler } from '../../application/handlers/role/update-role.handler'; +import { DeleteRoleHandler } from '../../application/handlers/role/delete-role.handler'; +import { RoleAssignPermissionHandler } from '../../application/handlers/role/assign-permission.handler'; +import { RoleRemovePermissionHandler } from '../../application/handlers/role/remove-permission.handler'; + +@ApiTags('Roles') +@Controller('roles') +@UseGuards(JwtAuthGuard, CurrentUserGuard, PermissionGuard) +export class RolesController { + constructor( + private readonly getRolesHandler: GetRolesHandler, + private readonly getRoleHandler: GetRoleHandler, + private readonly createRoleHandler: CreateRoleHandler, + private readonly updateRoleHandler: UpdateRoleHandler, + private readonly deleteRoleHandler: DeleteRoleHandler, + private readonly roleAssignPermissionHandler: RoleAssignPermissionHandler, + private readonly roleRemovePermissionHandler: RoleRemovePermissionHandler, + ) {} + + @Get() + @Permissions(PermissionType.ROLE_READ) + @ApiOperation({ summary: 'List roles' }) + async findAll(@Query() query: any) { + const res = await this.getRolesHandler.execute(query); + return { success: true, data: res.data, meta: { total: res.total } }; + } + + @Get(':id') + @Permissions(PermissionType.ROLE_READ) + @ApiOperation({ summary: 'Get role' }) + async findOne(@Param('id') id: string) { + const role = await this.getRoleHandler.execute(id); + return { success: true, data: role, meta: {} }; + } + + @Post() + @Permissions(PermissionType.ROLE_CREATE) + @ApiOperation({ summary: 'Create role' }) + async create(@Body() body: any) { + const created = await this.createRoleHandler.execute(body); + return { success: true, data: created, meta: {} }; + } + + @Patch(':id') + @Permissions(PermissionType.ROLE_UPDATE) + @ApiOperation({ summary: 'Update role' }) + async update(@Param('id') id: string, @Body() body: any) { + const updated = await this.updateRoleHandler.execute(id, body); + return { success: true, data: updated, meta: {} }; + } + + @Delete(':id') + @Permissions(PermissionType.ROLE_DELETE) + @ApiOperation({ summary: 'Delete role' }) + async remove(@Param('id') id: string) { + await this.deleteRoleHandler.execute(id); + return { success: true, data: null, meta: {} }; + } + + @Post(':id/permissions') + @Permissions(PermissionType.ROLE_UPDATE) + @ApiOperation({ summary: 'Assign permission to role' }) + async assignPermission(@Param('id') id: string, @Body() body: any) { + const permissionId = body.permissionId; + const updated = await this.roleAssignPermissionHandler.execute(id, permissionId); + return { success: true, data: updated, meta: {} }; + } + + @Delete(':id/permissions/:permissionId') + @Permissions(PermissionType.ROLE_UPDATE) + @ApiOperation({ summary: 'Remove permission from role' }) + async removePermission(@Param('id') id: string, @Param('permissionId') permissionId: string) { + const updated = await this.roleRemovePermissionHandler.execute(id, permissionId); + return { success: true, data: updated, meta: {} }; + } +} diff --git a/src/modules/identity/presentation/dto/create-permission.dto.ts b/src/modules/identity/presentation/dto/create-permission.dto.ts new file mode 100644 index 0000000..c030a48 --- /dev/null +++ b/src/modules/identity/presentation/dto/create-permission.dto.ts @@ -0,0 +1,19 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsString, IsNotEmpty, IsOptional } from 'class-validator'; + +export class CreatePermissionDto { + @ApiProperty() + @IsString() + @IsNotEmpty() + code!: string; + + @ApiProperty() + @IsString() + @IsNotEmpty() + name!: string; + + @ApiProperty({ required: false }) + @IsOptional() + @IsString() + description?: string; +} diff --git a/src/modules/identity/presentation/dto/create-role.dto.ts b/src/modules/identity/presentation/dto/create-role.dto.ts new file mode 100644 index 0000000..6104e72 --- /dev/null +++ b/src/modules/identity/presentation/dto/create-role.dto.ts @@ -0,0 +1,23 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsString, IsNotEmpty, IsOptional } from 'class-validator'; + +export class CreateRoleDto { + @ApiProperty() + @IsString() + @IsNotEmpty() + code!: string; + + @ApiProperty() + @IsString() + @IsNotEmpty() + name!: string; + + @ApiProperty({ required: false }) + @IsOptional() + @IsString() + description?: string; + + @ApiProperty({ required: false }) + @IsOptional() + isDefault?: boolean; +} diff --git a/src/modules/identity/presentation/dto/create-user.dto.ts b/src/modules/identity/presentation/dto/create-user.dto.ts index adcfdea..8996836 100644 --- a/src/modules/identity/presentation/dto/create-user.dto.ts +++ b/src/modules/identity/presentation/dto/create-user.dto.ts @@ -1,21 +1,27 @@ -import { IsEmail, IsNotEmpty, IsString, Length } from 'class-validator'; -import { ApiProperty } from '@nestjs/swagger'; +import { IsEmail, IsNotEmpty, IsString, Length, IsOptional, IsInt, Min } from 'class-validator'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; export class CreateUserDto { @ApiProperty({ example: 'John Doe' }) @IsString() @IsNotEmpty() - name!: string; + username!: string; @ApiProperty({ example: 'user@example.com' }) @IsEmail() email!: string; - @ApiProperty({ example: 'strongpassword' }) - @IsString() - @Length(8, 128) - password!: string; + - @ApiProperty({ required: false }) - metadata?: Record; + @ApiPropertyOptional({ example: 10737418240 }) + @IsOptional() + @IsInt() + @Min(0) + storageQuota?: number; + + @ApiPropertyOptional({ example: 0 }) + @IsOptional() + @IsInt() + @Min(0) + storageUsed?: number; } \ No newline at end of file diff --git a/src/modules/identity/presentation/dto/update-permission.dto.ts b/src/modules/identity/presentation/dto/update-permission.dto.ts new file mode 100644 index 0000000..a942d25 --- /dev/null +++ b/src/modules/identity/presentation/dto/update-permission.dto.ts @@ -0,0 +1,21 @@ +import { PartialType } from '@nestjs/mapped-types'; +import { CreatePermissionDto } from './create-permission.dto'; +import { ApiPropertyOptional } from '@nestjs/swagger'; +import { IsOptional, IsString } from 'class-validator'; + +export class UpdatePermissionDto extends PartialType(CreatePermissionDto) { + @ApiPropertyOptional() + @IsOptional() + @IsString() + name?: string; + + @ApiPropertyOptional() + @IsOptional() + @IsString() + description?: string; + + @ApiPropertyOptional() + @IsOptional() + @IsString() + code?: string; +} diff --git a/src/modules/identity/presentation/dto/update-role.dto.ts b/src/modules/identity/presentation/dto/update-role.dto.ts new file mode 100644 index 0000000..2a72472 --- /dev/null +++ b/src/modules/identity/presentation/dto/update-role.dto.ts @@ -0,0 +1,20 @@ +import { PartialType } from '@nestjs/mapped-types'; +import { CreateRoleDto } from './create-role.dto'; +import { ApiPropertyOptional } from '@nestjs/swagger'; +import { IsOptional, IsString } from 'class-validator'; + +export class UpdateRoleDto extends PartialType(CreateRoleDto) { + @ApiPropertyOptional() + @IsOptional() + @IsString() + name?: string; + + @ApiPropertyOptional() + @IsOptional() + @IsString() + description?: string; + + @ApiPropertyOptional() + @IsOptional() + isDefault?: boolean; +} diff --git a/src/modules/identity/presentation/dto/update-user.dto.ts b/src/modules/identity/presentation/dto/update-user.dto.ts index 1dbe93f..0fa3899 100644 --- a/src/modules/identity/presentation/dto/update-user.dto.ts +++ b/src/modules/identity/presentation/dto/update-user.dto.ts @@ -1,7 +1,7 @@ import { PartialType } from '@nestjs/mapped-types'; import { CreateUserDto } from './create-user.dto'; import { ApiPropertyOptional } from '@nestjs/swagger'; -import { IsOptional, IsString, IsEmail, Length } from 'class-validator'; +import { IsOptional, IsString, IsEmail, Length, IsInt, Min } from 'class-validator'; export class UpdateUserDto extends PartialType(CreateUserDto) { @ApiPropertyOptional() @@ -14,12 +14,21 @@ export class UpdateUserDto extends PartialType(CreateUserDto) { @IsEmail() email?: string; - @ApiPropertyOptional() - @IsOptional() - @Length(8, 128) - password?: string; + @ApiPropertyOptional() @IsOptional() metadata?: Record; + + @ApiPropertyOptional({ example: 10737418240 }) + @IsOptional() + @IsInt() + @Min(0) + storageQuota?: number; + + @ApiPropertyOptional({ example: 0 }) + @IsOptional() + @IsInt() + @Min(0) + storageUsed?: number; } diff --git a/src/modules/identity/presentation/guards/current-user.guard.ts b/src/modules/identity/presentation/guards/current-user.guard.ts new file mode 100644 index 0000000..a837015 --- /dev/null +++ b/src/modules/identity/presentation/guards/current-user.guard.ts @@ -0,0 +1,51 @@ +import { Injectable, CanActivate, ExecutionContext, UnauthorizedException, Inject } from '@nestjs/common'; +import { SyncIdentityHandler } from '../../application/handlers/user/sync-identity.handler'; +import { IdentityData } from '../../../../core/auth/interfaces/identity-data'; +import { IUser } from '../../domain/repositories/user.interface'; +import { AuthenticatedRequest, IdentitySource } from '../../../../core/auth/interfaces/authenticated-request.interface'; + +@Injectable() +export class CurrentUserGuard implements CanActivate { + constructor( + private readonly syncIdentityHandler: SyncIdentityHandler, + @Inject(IUser) private readonly userRepository: IUser, + ) {} + + async canActivate(context: ExecutionContext): Promise { + const request = context.switchToHttp().getRequest() as AuthenticatedRequest & { raylabContext?: any }; + + // Prefer RequestContext produced by AuthenticationService + const ctx = request.raylabContext as any; + if (ctx && ctx.user) { + request.currentUser = ctx.user; + return true; + } + + // Fallback to legacy identity if present + const identity = request.identity as IdentityData | undefined; + + if (!identity) { + throw new UnauthorizedException('Identity is missing.'); + } + + const source: IdentitySource | undefined = request.identitySource; + + if (source === 'internal') { + try { + const user = await this.userRepository.getById(identity.sub); + request.currentUser = user; + return true; + } catch (e) { + // convert repository not-found to Unauthorized + throw new UnauthorizedException('User not found.'); + } + } + + const user = await this.syncIdentityHandler.execute(identity); + + // attach domain user as currentUser + request.currentUser = user; + + return true; + } +} diff --git a/src/shared/cache-keys.ts b/src/shared/cache-keys.ts new file mode 100644 index 0000000..714d63a --- /dev/null +++ b/src/shared/cache-keys.ts @@ -0,0 +1,3 @@ +export const CacheKeys = { + permission: (userId: string) => `permissions:${userId}`, +}; diff --git a/src/shared/redis.service.ts b/src/shared/redis.service.ts new file mode 100644 index 0000000..ef5a3d5 --- /dev/null +++ b/src/shared/redis.service.ts @@ -0,0 +1,109 @@ +import { Injectable, OnModuleDestroy, OnModuleInit, Logger } from '@nestjs/common'; +import IORedis from 'ioredis'; + +@Injectable() +export class RedisService implements OnModuleInit, OnModuleDestroy { + // Keep client typed as any to avoid tight coupling to ioredis types in tests + private client: any = null; + private readonly logger = new Logger(RedisService.name); + private lastLogAt = 0; + private readonly LOG_THROTTLE_MS = 5000; // throttle repeated error logs + + onModuleInit() { + const enabled = (process.env.REDIS_ENABLED || 'true').toLowerCase() === 'true'; + if (!enabled) { + this.logger.log('Redis disabled via REDIS_ENABLED=false'); + return; + } + + const url = process.env.REDIS_URL || 'redis://localhost:6379'; + + // Use lazyConnect so application can start even if Redis is unavailable temporarily + this.client = new IORedis(url, { + lazyConnect: true, + // limit retries to avoid infinite reconnect storms + maxRetriesPerRequest: 5, + // automatic reconnection strategy + reconnectOnError: (err: any) => { + return true; + }, + enableOfflineQueue: true, + // optional reconnect strategy + retryStrategy: (times: number) => { + // exponential backoff capped at 5s + const delay = Math.min(50 * Math.pow(2, times), 5000); + return delay; + }, + }); + + this.client.on('connect', () => this.logger.log('Connected to Redis')); + this.client.on('ready', () => this.logger.log('Redis ready')); + this.client.on('error', (err: any) => this.handleError(err)); + this.client.on('close', () => this.logger.warn('Redis connection closed')); + this.client.on('reconnecting', () => this.logger.log('Redis reconnecting')); + + // attempt to connect but do not throw if it fails + this.client.connect().catch((err: any) => { + this.handleError(err); + }); + } + + private handleError(err: any) { + const now = Date.now(); + if (now - this.lastLogAt > this.LOG_THROTTLE_MS) { + this.logger.error('Redis error', err instanceof Error ? err.message : err); + this.lastLogAt = now; + } + } + + onModuleDestroy() { + if (this.client) { + try { + this.client.disconnect(); + } catch (e) { + // ignore + } + } + } + + getClient(): any { + return this.client; + } + + private ensureClient() { + // returns true if client is connected/usable + return this.client && this.client.status && this.client.status !== 'end' && this.client.status !== 'close'; + } + + async get(key: string): Promise { + if (!this.ensureClient()) return null; + try { + return await this.client.get(key); + } catch (e) { + this.handleError(e); + return null; + } + } + + async set(key: string, value: string, ttlSeconds?: number) { + if (!this.ensureClient()) return; + try { + if (ttlSeconds) { + await this.client.set(key, value, 'EX', ttlSeconds); + } else { + await this.client.set(key, value); + } + } catch (e) { + this.handleError(e); + } + } + + async del(key: string) { + if (!this.ensureClient()) return; + try { + await this.client.del(key); + } catch (e) { + this.handleError(e); + } + } +} diff --git a/src/shared/types/request-context.ts b/src/shared/types/request-context.ts new file mode 100644 index 0000000..8c15493 --- /dev/null +++ b/src/shared/types/request-context.ts @@ -0,0 +1,6 @@ +export interface RequestContext { + user: any; + identity: Record; + roles: string[]; // role ids + permissions: string[]; // permission codes +} diff --git a/test-results/.last-run.json b/test-results/.last-run.json new file mode 100644 index 0000000..d8b6315 --- /dev/null +++ b/test-results/.last-run.json @@ -0,0 +1,7 @@ +{ + "status": "failed", + "failedTests": [ + "c6e079ee66bcf8f961dc-c23b04c7b0e0ed54fd6c", + "c6e079ee66bcf8f961dc-eb7b68d84e422e1710d8" + ] +} \ No newline at end of file diff --git a/test-results/user.integration-Integrati-096a0-ser-and-issues-internal-JWT/error-context.md b/test-results/user.integration-Integrati-096a0-ser-and-issues-internal-JWT/error-context.md new file mode 100644 index 0000000..9978184 --- /dev/null +++ b/test-results/user.integration-Integrati-096a0-ser-and-issues-internal-JWT/error-context.md @@ -0,0 +1,193 @@ +# Instructions + +- Following Playwright test failed. +- Explain why, be concise, respect Playwright best practices. +- Provide a snippet of code with the fix, if possible. + +# Test info + +- Name: user.integration.spec.ts >> Integration - User lifecycle >> login via Authentik creates local user and issues internal JWT +- Location: tests\integration\user.integration.spec.ts:82:7 + +# Error details + +``` +Error: No jwt cookie set after login. Saved screenshot/cookies/page to tests/test-artifacts +``` + +# Test source + +```ts + 19 | // increase default timeout for slow integration flows + 20 | test.setTimeout(60000); + 21 | + 22 | async function adminLogin() { + 23 | const adminUser = process.env.ADMIN_USERNAME; + 24 | const adminPass = process.env.ADMIN_PASSWORD; + 25 | if (!adminUser || !adminPass) throw new Error('ADMIN_USERNAME/ADMIN_PASSWORD must be set in env for integration tests'); + 26 | + 27 | const browser = await chromium.launch({ headless: true }); + 28 | const context = await browser.newContext(); + 29 | const page = await context.newPage(); + 30 | await page.goto(BASE_URL, { waitUntil: 'networkidle' }); + 31 | + 32 | try { + 33 | await page.waitForSelector('input[type="text"], input[type="email"], input[name="username"]', { timeout: 10000 }); + 34 | const usernameInput = await page.$('input[name="username"]') || await page.$('input[type="email"]') || await page.$('input[type="text"]'); + 35 | if (usernameInput) await usernameInput.fill(adminUser); + 36 | const passwordInput = await page.$('input[type="password"], input[name="password"]'); + 37 | if (passwordInput) await passwordInput.fill(adminPass); + 38 | const submitButton = await page.$('button[type="submit"], input[type="submit"]'); + 39 | if (submitButton) { + 40 | await Promise.all([page.waitForNavigation({ waitUntil: 'networkidle', timeout: 20000 }), submitButton.click()]); + 41 | } + 42 | } catch (e) { + 43 | // ignore if login form not present + 44 | } + 45 | + 46 | await page.waitForTimeout(1000); + 47 | const cookies = await context.cookies(); + 48 | const jwtCookie = cookies.find(c => c.name === 'raylab_jwt'); + 49 | await context.close(); + 50 | await browser.close(); + 51 | + 52 | if (!jwtCookie) throw new Error('Admin login failed: no session cookie'); + 53 | return jwtCookie.value; + 54 | } + 55 | + 56 | async function apiRequest(path, token, opts = {}) { + 57 | const headers = Object.assign({ 'Content-Type': 'application/json', Authorization: `Bearer ${token}` }, opts.headers || {}); + 58 | const method = opts.method || 'GET'; + 59 | const body = opts.body ? JSON.stringify(opts.body) : undefined; + 60 | let url; + 61 | try { + 62 | url = new URL(path, BASE_URL).toString(); + 63 | } catch (e) { + 64 | throw new Error(`Invalid BASE_URL for integration tests: ${BASE_URL}`); + 65 | } + 66 | const res = await fetch(url, { method, headers, body }); + 67 | let json = null; + 68 | try { json = await res.json(); } catch (e) { json = null; } + 69 | return { status: res.status, body: json }; + 70 | } + 71 | + 72 | + 73 | test.describe('Integration - User lifecycle', () => { + 74 | let adminToken; + 75 | let createdUser = null; + 76 | + 77 | test.beforeAll(async () => { + 78 | // Integration tests assume test users are created in Authentik prior to running. + 79 | // RayLab will create local user record upon first successful login via Authentik. + 80 | }); + 81 | + 82 | test('login via Authentik creates local user and issues internal JWT', async () => { + 83 | // use browser flow to login as the test user + 84 | const browser = await chromium.launch({ headless: true }); + 85 | const context = await browser.newContext(); + 86 | const page = await context.newPage(); + 87 | await page.goto(BASE_URL, { waitUntil: 'networkidle' }); + 88 | + 89 | try { + 90 | await page.waitForSelector('input[type="text"], input[type="email"], input[name="username"]', { timeout: 10000 }); + 91 | const usernameInput = await page.$('input[name="username"]') || await page.$('input[type="email"]') || await page.$('input[type="text"]'); + 92 | if (usernameInput) await usernameInput.fill(TEST_USER_USERNAME); + 93 | const passwordInput = await page.$('input[type="password"], input[name="password"]'); + 94 | if (passwordInput) await passwordInput.fill(TEST_USER_PASSWORD); + 95 | const submitButton = await page.$('button[type="submit"], input[type="submit"]'); + 96 | if (submitButton) { + 97 | await Promise.all([page.waitForNavigation({ waitUntil: 'networkidle', timeout: 20000 }), submitButton.click()]); + 98 | } + 99 | } catch (e) { + 100 | // ignore if login form not present + 101 | } + 102 | + 103 | await page.waitForTimeout(1000); + 104 | const cookies = await context.cookies(); + 105 | const jwtCookie = cookies.find(c => c.name === 'raylab_jwt'); + 106 | const refreshCookie = cookies.find(c => c.name === 'raylab_refresh'); + 107 | + 108 | if (!jwtCookie) { + 109 | const fs = require('fs'); + 110 | const dir = 'tests/test-artifacts'; + 111 | if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); + 112 | const ts = Date.now(); + 113 | await page.screenshot({ path: `${dir}/failed-login-${ts}.png`, fullPage: true }); + 114 | fs.writeFileSync(`${dir}/failed-login-${ts}-cookies.json`, JSON.stringify(cookies, null, 2), 'utf-8'); + 115 | const content = await page.content(); + 116 | fs.writeFileSync(`${dir}/failed-login-${ts}-page.html`, content, 'utf-8'); + 117 | await context.close(); + 118 | await browser.close(); +> 119 | throw new Error(`No jwt cookie set after login. Saved screenshot/cookies/page to ${dir}`); + | ^ Error: No jwt cookie set after login. Saved screenshot/cookies/page to tests/test-artifacts + 120 | } + 121 | + 122 | // ensure we have an internal jwt cookie + 123 | expect(jwtCookie).toBeDefined(); + 124 | expect(jwtCookie.value).toBeTruthy(); + 125 | + 126 | // validate /auth/me using the internal jwt + 127 | const token = jwtCookie.value; + 128 | const meUrl = new URL('/auth/me', BASE_URL).toString(); + 129 | const meResp = await fetch(meUrl, { headers: { Authorization: `Bearer ${token}` } }); + 130 | const meJson = await meResp.json(); + 131 | expect(meResp.status).toBeLessThan(300); + 132 | expect(meJson.success).toBe(true); + 133 | expect(meJson.data).toBeDefined(); + 134 | expect(meJson.data.email).toBe(TEST_USER_EMAIL); + 135 | + 136 | await context.close(); + 137 | await browser.close(); + 138 | }); + 139 | + 140 | test('login via Authentik (created user) issues internal JWT', async () => { + 141 | // use browser flow to login as the created user + 142 | const browser = await chromium.launch({ headless: true }); + 143 | const context = await browser.newContext(); + 144 | const page = await context.newPage(); + 145 | await page.goto(BASE_URL, { waitUntil: 'networkidle' }); + 146 | + 147 | try { + 148 | await page.waitForSelector('input[type="text"], input[type="email"], input[name="username"]', { timeout: 10000 }); + 149 | const usernameInput = await page.$('input[name="username"]') || await page.$('input[type="email"]') || await page.$('input[type="text"]'); + 150 | if (usernameInput) await usernameInput.fill(TEST_USER_USERNAME); + 151 | const passwordInput = await page.$('input[type="password"], input[name="password"]'); + 152 | if (passwordInput) await passwordInput.fill(TEST_USER_PASSWORD); + 153 | const submitButton = await page.$('button[type="submit"], input[type="submit"]'); + 154 | if (submitButton) { + 155 | await Promise.all([page.waitForNavigation({ waitUntil: 'networkidle', timeout: 20000 }), submitButton.click()]); + 156 | } + 157 | } catch (e) { + 158 | // ignore if login form not present + 159 | } + 160 | + 161 | await page.waitForTimeout(1000); + 162 | const cookies = await context.cookies(); + 163 | const jwtCookie = cookies.find(c => c.name === 'raylab_jwt'); + 164 | + 165 | if (!jwtCookie) { + 166 | const fs = require('fs'); + 167 | const dir = 'tests/test-artifacts'; + 168 | if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); + 169 | const ts = Date.now(); + 170 | await page.screenshot({ path: `${dir}/failed-login-2-${ts}.png`, fullPage: true }); + 171 | fs.writeFileSync(`${dir}/failed-login-2-${ts}-cookies.json`, JSON.stringify(cookies, null, 2), 'utf-8'); + 172 | const content = await page.content(); + 173 | fs.writeFileSync(`${dir}/failed-login-2-${ts}-page.html`, content, 'utf-8'); + 174 | await context.close(); + 175 | await browser.close(); + 176 | throw new Error(`No jwt cookie set after login (second test). Saved screenshot/cookies/page to ${dir}`); + 177 | } + 178 | + 179 | await context.close(); + 180 | await browser.close(); + 181 | + 182 | expect(jwtCookie).toBeDefined(); + 183 | expect(jwtCookie.value).toBeTruthy(); + 184 | }); + 185 | + 186 | // Cleanup via API provisioning has been removed. If test environment requires cleanup, perform manually in Authentik. + 187 | + 188 | }); + 189 | +``` \ No newline at end of file diff --git a/test-results/user.integration-Integrati-8202b-ed-user-issues-internal-JWT/error-context.md b/test-results/user.integration-Integrati-8202b-ed-user-issues-internal-JWT/error-context.md new file mode 100644 index 0000000..18988bc --- /dev/null +++ b/test-results/user.integration-Integrati-8202b-ed-user-issues-internal-JWT/error-context.md @@ -0,0 +1,136 @@ +# Instructions + +- Following Playwright test failed. +- Explain why, be concise, respect Playwright best practices. +- Provide a snippet of code with the fix, if possible. + +# Test info + +- Name: user.integration.spec.ts >> Integration - User lifecycle >> login via Authentik (created user) issues internal JWT +- Location: tests\integration\user.integration.spec.ts:140:7 + +# Error details + +``` +Error: No jwt cookie set after login (second test). Saved screenshot/cookies/page to tests/test-artifacts +``` + +# Test source + +```ts + 76 | + 77 | test.beforeAll(async () => { + 78 | // Integration tests assume test users are created in Authentik prior to running. + 79 | // RayLab will create local user record upon first successful login via Authentik. + 80 | }); + 81 | + 82 | test('login via Authentik creates local user and issues internal JWT', async () => { + 83 | // use browser flow to login as the test user + 84 | const browser = await chromium.launch({ headless: true }); + 85 | const context = await browser.newContext(); + 86 | const page = await context.newPage(); + 87 | await page.goto(BASE_URL, { waitUntil: 'networkidle' }); + 88 | + 89 | try { + 90 | await page.waitForSelector('input[type="text"], input[type="email"], input[name="username"]', { timeout: 10000 }); + 91 | const usernameInput = await page.$('input[name="username"]') || await page.$('input[type="email"]') || await page.$('input[type="text"]'); + 92 | if (usernameInput) await usernameInput.fill(TEST_USER_USERNAME); + 93 | const passwordInput = await page.$('input[type="password"], input[name="password"]'); + 94 | if (passwordInput) await passwordInput.fill(TEST_USER_PASSWORD); + 95 | const submitButton = await page.$('button[type="submit"], input[type="submit"]'); + 96 | if (submitButton) { + 97 | await Promise.all([page.waitForNavigation({ waitUntil: 'networkidle', timeout: 20000 }), submitButton.click()]); + 98 | } + 99 | } catch (e) { + 100 | // ignore if login form not present + 101 | } + 102 | + 103 | await page.waitForTimeout(1000); + 104 | const cookies = await context.cookies(); + 105 | const jwtCookie = cookies.find(c => c.name === 'raylab_jwt'); + 106 | const refreshCookie = cookies.find(c => c.name === 'raylab_refresh'); + 107 | + 108 | if (!jwtCookie) { + 109 | const fs = require('fs'); + 110 | const dir = 'tests/test-artifacts'; + 111 | if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); + 112 | const ts = Date.now(); + 113 | await page.screenshot({ path: `${dir}/failed-login-${ts}.png`, fullPage: true }); + 114 | fs.writeFileSync(`${dir}/failed-login-${ts}-cookies.json`, JSON.stringify(cookies, null, 2), 'utf-8'); + 115 | const content = await page.content(); + 116 | fs.writeFileSync(`${dir}/failed-login-${ts}-page.html`, content, 'utf-8'); + 117 | await context.close(); + 118 | await browser.close(); + 119 | throw new Error(`No jwt cookie set after login. Saved screenshot/cookies/page to ${dir}`); + 120 | } + 121 | + 122 | // ensure we have an internal jwt cookie + 123 | expect(jwtCookie).toBeDefined(); + 124 | expect(jwtCookie.value).toBeTruthy(); + 125 | + 126 | // validate /auth/me using the internal jwt + 127 | const token = jwtCookie.value; + 128 | const meUrl = new URL('/auth/me', BASE_URL).toString(); + 129 | const meResp = await fetch(meUrl, { headers: { Authorization: `Bearer ${token}` } }); + 130 | const meJson = await meResp.json(); + 131 | expect(meResp.status).toBeLessThan(300); + 132 | expect(meJson.success).toBe(true); + 133 | expect(meJson.data).toBeDefined(); + 134 | expect(meJson.data.email).toBe(TEST_USER_EMAIL); + 135 | + 136 | await context.close(); + 137 | await browser.close(); + 138 | }); + 139 | + 140 | test('login via Authentik (created user) issues internal JWT', async () => { + 141 | // use browser flow to login as the created user + 142 | const browser = await chromium.launch({ headless: true }); + 143 | const context = await browser.newContext(); + 144 | const page = await context.newPage(); + 145 | await page.goto(BASE_URL, { waitUntil: 'networkidle' }); + 146 | + 147 | try { + 148 | await page.waitForSelector('input[type="text"], input[type="email"], input[name="username"]', { timeout: 10000 }); + 149 | const usernameInput = await page.$('input[name="username"]') || await page.$('input[type="email"]') || await page.$('input[type="text"]'); + 150 | if (usernameInput) await usernameInput.fill(TEST_USER_USERNAME); + 151 | const passwordInput = await page.$('input[type="password"], input[name="password"]'); + 152 | if (passwordInput) await passwordInput.fill(TEST_USER_PASSWORD); + 153 | const submitButton = await page.$('button[type="submit"], input[type="submit"]'); + 154 | if (submitButton) { + 155 | await Promise.all([page.waitForNavigation({ waitUntil: 'networkidle', timeout: 20000 }), submitButton.click()]); + 156 | } + 157 | } catch (e) { + 158 | // ignore if login form not present + 159 | } + 160 | + 161 | await page.waitForTimeout(1000); + 162 | const cookies = await context.cookies(); + 163 | const jwtCookie = cookies.find(c => c.name === 'raylab_jwt'); + 164 | + 165 | if (!jwtCookie) { + 166 | const fs = require('fs'); + 167 | const dir = 'tests/test-artifacts'; + 168 | if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); + 169 | const ts = Date.now(); + 170 | await page.screenshot({ path: `${dir}/failed-login-2-${ts}.png`, fullPage: true }); + 171 | fs.writeFileSync(`${dir}/failed-login-2-${ts}-cookies.json`, JSON.stringify(cookies, null, 2), 'utf-8'); + 172 | const content = await page.content(); + 173 | fs.writeFileSync(`${dir}/failed-login-2-${ts}-page.html`, content, 'utf-8'); + 174 | await context.close(); + 175 | await browser.close(); +> 176 | throw new Error(`No jwt cookie set after login (second test). Saved screenshot/cookies/page to ${dir}`); + | ^ Error: No jwt cookie set after login (second test). Saved screenshot/cookies/page to tests/test-artifacts + 177 | } + 178 | + 179 | await context.close(); + 180 | await browser.close(); + 181 | + 182 | expect(jwtCookie).toBeDefined(); + 183 | expect(jwtCookie.value).toBeTruthy(); + 184 | }); + 185 | + 186 | // Cleanup via API provisioning has been removed. If test environment requires cleanup, perform manually in Authentik. + 187 | + 188 | }); + 189 | +``` \ No newline at end of file diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 0000000..8f482be --- /dev/null +++ b/tests/README.md @@ -0,0 +1,12 @@ +Playwright API Testing for RayLab Core + +Structure: +- helpers: auth/request helpers +- fixtures: reusable data helpers +- api: per-controller tests +- reporter: custom reporter writing tests/output/latest-result.txt + +Run: + npx playwright test + +Ensure .env is configured with BASE_URL and identity provider (Authentik or compatible) details. diff --git a/prisma/migrations/.gitkeep b/tests/api/permissions/.gitkeep similarity index 100% rename from prisma/migrations/.gitkeep rename to tests/api/permissions/.gitkeep diff --git a/src/modules/identity/domain/.gitkeep b/tests/api/roles/.gitkeep similarity index 100% rename from src/modules/identity/domain/.gitkeep rename to tests/api/roles/.gitkeep diff --git a/tests/api/users/enable-user.spec.ts b/tests/api/users/enable-user.spec.ts new file mode 100644 index 0000000..623dc56 --- /dev/null +++ b/tests/api/users/enable-user.spec.ts @@ -0,0 +1,11 @@ +import test from '@playwright/test'; +import { requestPatch } from '../../helpers/request'; + +const { expect } = test; + +test.describe('Users - PATCH /users/:id/enable', () => { + test('should return 404 for non-existing user', async () => { + const res = await requestPatch('/users/non-existing-id/enable'); + expect([404, 400, 500]).toContain(res.status); + }); +}); diff --git a/tests/api/users/get-current-user.spec.ts b/tests/api/users/get-current-user.spec.ts new file mode 100644 index 0000000..e562eba --- /dev/null +++ b/tests/api/users/get-current-user.spec.ts @@ -0,0 +1,20 @@ +import test from '@playwright/test'; +import { requestGet } from '../../helpers/request'; + +const { expect } = test; + +test.describe('Users - GET /users/me', () => { + test('should return current user when token provided', async () => { + const res = await requestGet('/users/me'); + expect(res.status).toBeGreaterThanOrEqual(200); + expect(res.status).toBeLessThan(300); + expect(res.body.success).toBe(true); + expect(res.body.data).toBeDefined(); + expect(res.body.data.email).toBeTruthy(); + }); + + test('should be unauthorized without token', async () => { + const res = await requestGet('/users/me', null); + expect(res.status).toBe(401); + }); +}); diff --git a/tests/api/users/get-user.spec.ts b/tests/api/users/get-user.spec.ts new file mode 100644 index 0000000..8333624 --- /dev/null +++ b/tests/api/users/get-user.spec.ts @@ -0,0 +1,11 @@ +import test from '@playwright/test'; +import { requestGet } from '../../helpers/request'; + +const { expect } = test; + +test.describe('Users - GET /users/:id', () => { + test('should return 404 for non-existing user', async () => { + const res = await requestGet('/users/non-existing-id'); + expect([404, 400, 500]).toContain(res.status); + }); +}); diff --git a/tests/api/users/get-users.spec.ts b/tests/api/users/get-users.spec.ts new file mode 100644 index 0000000..4855d82 --- /dev/null +++ b/tests/api/users/get-users.spec.ts @@ -0,0 +1,23 @@ +import test from '@playwright/test'; +import { requestGet } from '../../helpers/request'; + + +const { expect } = test; + +test.describe('Users - GET /users', () => { + test('should return list of users (authorized)', async () => { + const res = await requestGet('/users'); + + expect(res.status).toBeGreaterThanOrEqual(200); + expect(res.status).toBeLessThan(300); + expect(res.body).toBeTruthy(); + expect(res.body.success).toBe(true); + expect(res.body.data).toBeInstanceOf(Array); + expect(res.body.meta).toBeDefined(); + }); + + test('should return unauthorized when missing token', async () => { + const res = await requestGet('/users', null); + expect(res.status).toBe(401); + }); +}); diff --git a/tests/authentication.service.spec.ts b/tests/authentication.service.spec.ts new file mode 100644 index 0000000..a402bb3 --- /dev/null +++ b/tests/authentication.service.spec.ts @@ -0,0 +1,38 @@ +import { AuthenticationService } from '../src/modules/auth/authentication.service'; + +describe('AuthenticationService', () => { + let service: AuthenticationService; + const mockOidc: any = { verifyToken: jest.fn() }; + const mockPrisma: any = {}; + const mockRoleSync: any = { syncUserRolesFromAuthentik: jest.fn() }; + const mockAuthz: any = { getUserPermissions: jest.fn() }; + const mockEvents: any = { publish: jest.fn() }; + + beforeEach(() => { + mockPrisma.user = { findUnique: jest.fn(), create: jest.fn() }; + mockPrisma.userRole = { findMany: jest.fn() }; + + service = new AuthenticationService(mockOidc as any, mockPrisma as any, mockRoleSync as any, mockAuthz as any, mockEvents as any); + }); + + test('authenticate creates context on valid token', async () => { + const token = 'valid'; + mockOidc.verifyToken.mockResolvedValue({ sub: 'sub1', email: 'u@example.com', groups: ['RL-Owner'] }); + mockPrisma.user.findUnique.mockResolvedValue(null); + mockPrisma.user.create.mockResolvedValue({ id: 'uid', authentikId: 'sub1', email: 'u@example.com' }); + mockRoleSync.syncUserRolesFromAuthentik.mockResolvedValue({ skipped: false, assignedRoleIds: ['r1'] }); + mockAuthz.getUserPermissions.mockResolvedValue(['users.read']); + mockPrisma.userRole.findMany.mockResolvedValue([{ roleId: 'r1' }]); + + const ctx = await service.authenticate(token); + + expect(ctx.user.id).toBe('uid'); + expect(ctx.permissions).toContain('users.read'); + expect(mockEvents.publish).toHaveBeenCalledWith('user.authenticated', expect.any(Object)); + }); + + test('authenticate throws for missing sub', async () => { + mockOidc.verifyToken.mockResolvedValue({ email: 'u@example.com' }); + await expect(service.authenticate('bad')).rejects.toThrow('Invalid token: missing sub'); + }); +}); diff --git a/tests/authorization.service.spec.ts b/tests/authorization.service.spec.ts new file mode 100644 index 0000000..a6d9736 --- /dev/null +++ b/tests/authorization.service.spec.ts @@ -0,0 +1,34 @@ +import { AuthorizationService, PERMISSION_CACHE } from '../src/modules/authorization/authorization.service'; + +describe('AuthorizationService', () => { + let service: AuthorizationService; + const mockPrisma: any = { $queryRaw: jest.fn(), $queryRawUnsafe: jest.fn() }; + const mockCache: any = { get: jest.fn(), set: jest.fn(), invalidate: jest.fn() }; + + beforeEach(() => { + service = new AuthorizationService(mockPrisma as any, mockCache as any); + }); + + test('getUserPermissions uses cache when available', async () => { + mockCache.get.mockResolvedValue(['users.read']); + const perms = await service.getUserPermissions('uid'); + expect(perms).toEqual(['users.read']); + expect(mockCache.get).toHaveBeenCalledWith('uid'); + }); + + test('getUserPermissions queries DB and sets cache when missing', async () => { + mockCache.get.mockResolvedValue(null); + mockPrisma.$queryRaw = jest.fn().mockResolvedValue([{ code: 'users.read' }]); + const perms = await service.getUserPermissions('uid'); + expect(perms).toEqual(['users.read']); + expect(mockCache.set).toHaveBeenCalledWith('uid', ['users.read']); + }); + + test('hasPermission returns correct boolean', async () => { + jest.spyOn(service, 'getUserPermissions' as any).mockResolvedValue(['users.read']); + const ok = await service.hasPermission('uid', 'users.read'); + expect(ok).toBe(true); + const nok = await service.hasPermission('uid', 'users.delete'); + expect(nok).toBe(false); + }); +}); diff --git a/tests/fixtures/user.fixture.ts b/tests/fixtures/user.fixture.ts new file mode 100644 index 0000000..74d47ae --- /dev/null +++ b/tests/fixtures/user.fixture.ts @@ -0,0 +1,11 @@ +import { requestPost, requestDelete } from '../helpers/request'; + +export async function createDummyUser(payload: any) { + const res = await requestPost('/user', 'admin', payload); + return res; +} + +export async function deleteDummyUser(userId: string) { + // soft delete + await requestDelete(`/users/${userId}`, 'admin'); +} diff --git a/tests/helpers/auth.ts b/tests/helpers/auth.ts new file mode 100644 index 0000000..d9590eb --- /dev/null +++ b/tests/helpers/auth.ts @@ -0,0 +1,91 @@ +import { chromium } from 'playwright'; +import { env } from './env'; + +const store: Map = new Map(); + +function setToken(key: string, token: string, expiresIn?: number) { + const expiresAt = expiresIn ? Date.now() + expiresIn * 1000 - 5000 : undefined; + store.set(key, { token, expiresAt }); +} + +function getTokenFromStore(key: string) { + const entry = store.get(key); + if (!entry) return null; + if (entry.expiresAt && Date.now() > entry.expiresAt) { + store.delete(key); + return null; + } + return entry.token; +} + +async function loginViaRaylab(username: string, password: string) { + const base = env.BASE_URL || 'http://localhost:3000'; + const browser = await chromium.launch({ headless: Boolean(process.env.PW_HEADLESS || '1') }); + const context = await browser.newContext(); + const page = await context.newPage(); + + // Navigate to RayLab - expects redirect to identity provider + await page.goto(base, { waitUntil: 'networkidle' }); + + // Wait for a login form to appear on identity provider + try { + await page.waitForSelector('input[type="text"], input[type="email"], input[name="username"]', { timeout: 10000 }); + // fill username + const usernameInput = await page.$('input[name="username"]') || await page.$('input[type="email"]') || await page.$('input[type="text"]'); + if (usernameInput) await usernameInput.fill(username); + + // fill password + const passwordInput = await page.$('input[type="password"], input[name="password"]'); + if (passwordInput) await passwordInput.fill(password); + + // try to submit + const submitButton = await page.$('button[type="submit"], input[type="submit"]'); + if (submitButton) { + await Promise.all([page.waitForNavigation({ waitUntil: 'networkidle', timeout: 20000 }), submitButton.click()]); + } + } catch (e) { + // If no login form found, we may already be at callback + } + + // Wait for callback to be done and cookie to be set + await page.waitForTimeout(1000); + const cookies = await context.cookies(); + const jwtCookie = cookies.find(c => c.name === 'raylab_jwt'); + const refreshCookie = cookies.find(c => c.name === 'raylab_refresh'); + + const result: any = {}; + if (jwtCookie) result.accessToken = jwtCookie.value; + if (refreshCookie) result.refreshToken = refreshCookie.value; + + await context.close(); + await browser.close(); + + if (!result.accessToken) throw new Error('Login failed: no session cookie set'); + + return result; +} + +export async function getToken(role: 'owner' | 'admin' | 'employee' = 'admin') { + const cacheKey = `role:${role}`; + const cached = getTokenFromStore(cacheKey); + if (cached) return cached; + + const username = role === 'owner' ? env.OWNER_USERNAME : role === 'admin' ? env.ADMIN_USERNAME : env.EMPLOYEE_USERNAME; + const password = role === 'owner' ? env.OWNER_PASSWORD : role === 'admin' ? env.ADMIN_PASSWORD : env.EMPLOYEE_PASSWORD; + + if (!username || !password) throw new Error('Missing credentials in .env for role: ' + role); + + const data = await loginViaRaylab(username, password); + const token = data.accessToken; + // expiresIn not available from cookie - default to 1 hour + const expiresIn = Number(process.env.TEST_TOKEN_EXPIRES_IN || 3600); + setToken(cacheKey, token, expiresIn); + return token; +} + +export async function getRawToken(username: string, password: string) { + const data = await loginViaRaylab(username, password); + return data.accessToken; +} + +export default { getToken, getRawToken }; diff --git a/tests/helpers/env.ts b/tests/helpers/env.ts new file mode 100644 index 0000000..eab59dd --- /dev/null +++ b/tests/helpers/env.ts @@ -0,0 +1,7 @@ +import dotenv from 'dotenv'; +import path from 'path'; + +const envPath = path.resolve(process.cwd(), '.env'); +dotenv.config({ path: envPath }); + +export const env = process.env; diff --git a/tests/helpers/index.ts b/tests/helpers/index.ts new file mode 100644 index 0000000..63ad0e0 --- /dev/null +++ b/tests/helpers/index.ts @@ -0,0 +1,3 @@ +export * from './auth'; +export * from './request'; +export * from './env'; diff --git a/tests/helpers/request.ts b/tests/helpers/request.ts new file mode 100644 index 0000000..eb92b0d --- /dev/null +++ b/tests/helpers/request.ts @@ -0,0 +1,73 @@ +import { request } from '@playwright/test'; +import { env } from './env'; +import { getToken } from './auth'; + +async function buildContext(token?: string) { + const headers: any = { 'Content-Type': 'application/json' }; + if (token) headers['Authorization'] = `Bearer ${token}`; + + const api = await request.newContext({ baseURL: env.BASE_URL || 'http://localhost:3000', extraHTTPHeaders: headers }); + return api; +} + +export async function requestGet(path: string, role: 'owner' | 'admin' | 'employee' | null = 'admin', opts: any = {}) { + const token = role ? await getToken(role) : undefined; + const api = await buildContext(token); + try { + const res = await api.get(path, opts); + const body = await parseResponseSafe(res); + return { status: res.status(), body, headers: res.headers() }; + } finally { + await api.dispose(); + } +} + +export async function requestPost(path: string, role: 'owner' | 'admin' | 'employee' | null = 'admin', data?: any, opts: any = {}) { + const token = role ? await getToken(role) : undefined; + const api = await buildContext(token); + try { + const res = await api.post(path, { data, ...opts }); + const body = await parseResponseSafe(res); + return { status: res.status(), body, headers: res.headers() }; + } finally { + await api.dispose(); + } +} + +export async function requestPatch(path: string, role: 'owner' | 'admin' | 'employee' | null = 'admin', data?: any, opts: any = {}) { + const token = role ? await getToken(role) : undefined; + const api = await buildContext(token); + try { + const res = await api.patch(path, { data, ...opts }); + const body = await parseResponseSafe(res); + return { status: res.status(), body, headers: res.headers() }; + } finally { + await api.dispose(); + } +} + +export async function requestDelete(path: string, role: 'owner' | 'admin' | 'employee' | null = 'admin', opts: any = {}) { + const token = role ? await getToken(role) : undefined; + const api = await buildContext(token); + try { + const res = await api.delete(path, opts); + const body = await parseResponseSafe(res); + return { status: res.status(), body, headers: res.headers() }; + } finally { + await api.dispose(); + } +} + +async function parseResponseSafe(res: any) { + const ct = res.headers()['content-type'] || ''; + try { + if (ct.includes('application/json')) return await res.json(); + return await res.text(); + } catch (e) { + try { + return await res.text(); + } catch (e2) { + return null; + } + } +} diff --git a/tests/integration/user.integration.spec.ts b/tests/integration/user.integration.spec.ts new file mode 100644 index 0000000..62bf886 --- /dev/null +++ b/tests/integration/user.integration.spec.ts @@ -0,0 +1,188 @@ +import dotenv from 'dotenv'; +import fetch from 'node-fetch'; +import { test, expect } from '@playwright/test'; +import { chromium } from 'playwright'; + +dotenv.config(); + +const TEST_USER_EMAIL = process.env.TEST_USER_EMAIL || 'test-integration@example.com'; +const TEST_USER_USERNAME = process.env.TEST_USER_USERNAME || 'test-integration'; +const TEST_USER_PASSWORD = process.env.TEST_USER_PASSWORD || 'StrongP@ssw0rd!'; + +let BASE_URL = (process.env.BASE_URL || 'http://localhost:3000').trim(); +// sometimes env can contain accidental concatenated vars; take first token +BASE_URL = BASE_URL.split(/\s+/)[0]; +if (!BASE_URL.startsWith('http://') && !BASE_URL.startsWith('https://')) { + BASE_URL = 'http://' + BASE_URL; +} + +// increase default timeout for slow integration flows +test.setTimeout(60000); + +async function adminLogin() { + const adminUser = process.env.ADMIN_USERNAME; + const adminPass = process.env.ADMIN_PASSWORD; + if (!adminUser || !adminPass) throw new Error('ADMIN_USERNAME/ADMIN_PASSWORD must be set in env for integration tests'); + + const browser = await chromium.launch({ headless: true }); + const context = await browser.newContext(); + const page = await context.newPage(); + await page.goto(BASE_URL, { waitUntil: 'networkidle' }); + + try { + await page.waitForSelector('input[type="text"], input[type="email"], input[name="username"]', { timeout: 10000 }); + const usernameInput = await page.$('input[name="username"]') || await page.$('input[type="email"]') || await page.$('input[type="text"]'); + if (usernameInput) await usernameInput.fill(adminUser); + const passwordInput = await page.$('input[type="password"], input[name="password"]'); + if (passwordInput) await passwordInput.fill(adminPass); + const submitButton = await page.$('button[type="submit"], input[type="submit"]'); + if (submitButton) { + await Promise.all([page.waitForNavigation({ waitUntil: 'networkidle', timeout: 20000 }), submitButton.click()]); + } + } catch (e) { + // ignore if login form not present + } + + await page.waitForTimeout(1000); + const cookies = await context.cookies(); + const jwtCookie = cookies.find(c => c.name === 'raylab_jwt'); + await context.close(); + await browser.close(); + + if (!jwtCookie) throw new Error('Admin login failed: no session cookie'); + return jwtCookie.value; +} + +async function apiRequest(path, token, opts = {}) { + const headers = Object.assign({ 'Content-Type': 'application/json', Authorization: `Bearer ${token}` }, opts.headers || {}); + const method = opts.method || 'GET'; + const body = opts.body ? JSON.stringify(opts.body) : undefined; + let url; + try { + url = new URL(path, BASE_URL).toString(); + } catch (e) { + throw new Error(`Invalid BASE_URL for integration tests: ${BASE_URL}`); + } + const res = await fetch(url, { method, headers, body }); + let json = null; + try { json = await res.json(); } catch (e) { json = null; } + return { status: res.status, body: json }; +} + + +test.describe('Integration - User lifecycle', () => { + let adminToken; + let createdUser = null; + + test.beforeAll(async () => { + // Integration tests assume test users are created in Authentik prior to running. + // RayLab will create local user record upon first successful login via Authentik. + }); + + test('login via Authentik creates local user and issues internal JWT', async () => { + // use browser flow to login as the test user + const browser = await chromium.launch({ headless: true }); + const context = await browser.newContext(); + const page = await context.newPage(); + await page.goto(BASE_URL, { waitUntil: 'networkidle' }); + + try { + await page.waitForSelector('input[type="text"], input[type="email"], input[name="username"]', { timeout: 10000 }); + const usernameInput = await page.$('input[name="username"]') || await page.$('input[type="email"]') || await page.$('input[type="text"]'); + if (usernameInput) await usernameInput.fill(TEST_USER_USERNAME); + const passwordInput = await page.$('input[type="password"], input[name="password"]'); + if (passwordInput) await passwordInput.fill(TEST_USER_PASSWORD); + const submitButton = await page.$('button[type="submit"], input[type="submit"]'); + if (submitButton) { + await Promise.all([page.waitForNavigation({ waitUntil: 'networkidle', timeout: 20000 }), submitButton.click()]); + } + } catch (e) { + // ignore if login form not present + } + + await page.waitForTimeout(1000); + const cookies = await context.cookies(); + const jwtCookie = cookies.find(c => c.name === 'raylab_jwt'); + const refreshCookie = cookies.find(c => c.name === 'raylab_refresh'); + + if (!jwtCookie) { + const fs = require('fs'); + const dir = 'tests/test-artifacts'; + if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); + const ts = Date.now(); + await page.screenshot({ path: `${dir}/failed-login-${ts}.png`, fullPage: true }); + fs.writeFileSync(`${dir}/failed-login-${ts}-cookies.json`, JSON.stringify(cookies, null, 2), 'utf-8'); + const content = await page.content(); + fs.writeFileSync(`${dir}/failed-login-${ts}-page.html`, content, 'utf-8'); + await context.close(); + await browser.close(); + throw new Error(`No jwt cookie set after login. Saved screenshot/cookies/page to ${dir}`); + } + + // ensure we have an internal jwt cookie + expect(jwtCookie).toBeDefined(); + expect(jwtCookie.value).toBeTruthy(); + + // validate /auth/me using the internal jwt + const token = jwtCookie.value; + const meUrl = new URL('/auth/me', BASE_URL).toString(); + const meResp = await fetch(meUrl, { headers: { Authorization: `Bearer ${token}` } }); + const meJson = await meResp.json(); + expect(meResp.status).toBeLessThan(300); + expect(meJson.success).toBe(true); + expect(meJson.data).toBeDefined(); + expect(meJson.data.email).toBe(TEST_USER_EMAIL); + + await context.close(); + await browser.close(); + }); + + test('login via Authentik (created user) issues internal JWT', async () => { + // use browser flow to login as the created user + const browser = await chromium.launch({ headless: true }); + const context = await browser.newContext(); + const page = await context.newPage(); + await page.goto(BASE_URL, { waitUntil: 'networkidle' }); + + try { + await page.waitForSelector('input[type="text"], input[type="email"], input[name="username"]', { timeout: 10000 }); + const usernameInput = await page.$('input[name="username"]') || await page.$('input[type="email"]') || await page.$('input[type="text"]'); + if (usernameInput) await usernameInput.fill(TEST_USER_USERNAME); + const passwordInput = await page.$('input[type="password"], input[name="password"]'); + if (passwordInput) await passwordInput.fill(TEST_USER_PASSWORD); + const submitButton = await page.$('button[type="submit"], input[type="submit"]'); + if (submitButton) { + await Promise.all([page.waitForNavigation({ waitUntil: 'networkidle', timeout: 20000 }), submitButton.click()]); + } + } catch (e) { + // ignore if login form not present + } + + await page.waitForTimeout(1000); + const cookies = await context.cookies(); + const jwtCookie = cookies.find(c => c.name === 'raylab_jwt'); + + if (!jwtCookie) { + const fs = require('fs'); + const dir = 'tests/test-artifacts'; + if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); + const ts = Date.now(); + await page.screenshot({ path: `${dir}/failed-login-2-${ts}.png`, fullPage: true }); + fs.writeFileSync(`${dir}/failed-login-2-${ts}-cookies.json`, JSON.stringify(cookies, null, 2), 'utf-8'); + const content = await page.content(); + fs.writeFileSync(`${dir}/failed-login-2-${ts}-page.html`, content, 'utf-8'); + await context.close(); + await browser.close(); + throw new Error(`No jwt cookie set after login (second test). Saved screenshot/cookies/page to ${dir}`); + } + + await context.close(); + await browser.close(); + + expect(jwtCookie).toBeDefined(); + expect(jwtCookie.value).toBeTruthy(); + }); + + // Cleanup via API provisioning has been removed. If test environment requires cleanup, perform manually in Authentik. + +}); diff --git a/src/modules/identity/events/.gitkeep b/tests/output/.gitkeep similarity index 100% rename from src/modules/identity/events/.gitkeep rename to tests/output/.gitkeep diff --git a/tests/output/latest-result.txt b/tests/output/latest-result.txt new file mode 100644 index 0000000..32528bb --- /dev/null +++ b/tests/output/latest-result.txt @@ -0,0 +1 @@ +TEST RUN FAILED OR ABORTED \ No newline at end of file diff --git a/tests/reporter/custom-reporter.js b/tests/reporter/custom-reporter.js new file mode 100644 index 0000000..3e3569d --- /dev/null +++ b/tests/reporter/custom-reporter.js @@ -0,0 +1,72 @@ +const fs = require('fs'); + +class CustomReporter { + onEnd(config, result) { + const outputPath = 'tests/output/latest-result.txt'; + + if (!result) { + // test run aborted or failed to initialize + if (!fs.existsSync('tests/output')) fs.mkdirSync('tests/output', { recursive: true }); + fs.writeFileSync(outputPath, 'TEST RUN FAILED OR ABORTED', 'utf-8'); + return; + } + + const failed = result.status === 'failed' || result.numFailedTests > 0; + + const date = new Date().toISOString(); + const duration = (result.duration || 0) + 'ms'; + + if (!fs.existsSync('tests/output')) fs.mkdirSync('tests/output', { recursive: true }); + + if (!failed) { + const content = [ + '==================================', + 'TEST RESULT', + 'PASSED', + `Date: ${date}`, + `Total Test: ${result.total || 0}`, + `Passed: ${result.passed || 0}`, + `Failed: ${result.failed || 0}`, + `Duration: ${duration}`, + '==================================', + ].join('\n'); + + fs.writeFileSync(outputPath, content, 'utf-8'); + return; + } + + const lines = []; + lines.push('=================================='); + lines.push('FAILED'); + lines.push(`Date: ${date}`); + lines.push(`Duration: ${duration}`); + lines.push('=================================='); + + const buildFailureDetails = (suites) => { + for (const s of suites) { + if (s.suites && s.suites.length) buildFailureDetails(s.suites); + if (s.tests && s.tests.length) { + for (const t of s.tests) { + if (t.status === 'failed') { + lines.push('Test Name: ' + (t.title || t.titleText || '')); + for (const r of t.results || []) { + if (r.status === 'failed') { + const error = r.error || {}; + lines.push('Error Message: ' + (error.message || '')); + if (error.stack) lines.push('Stack Trace: ' + error.stack); + } + } + lines.push('----------------------------------'); + } + } + } + } + }; + + if (result.suites) buildFailureDetails(result.suites); + + fs.writeFileSync(outputPath, lines.join('\n'), 'utf-8'); + } +} + +module.exports = CustomReporter; diff --git a/tests/reporter/custom-reporter.ts b/tests/reporter/custom-reporter.ts new file mode 100644 index 0000000..fff0d6a --- /dev/null +++ b/tests/reporter/custom-reporter.ts @@ -0,0 +1,78 @@ +import fs from 'fs'; +import { FullConfig, Reporter, Suite, TestCase, TestError } from '@playwright/test/reporter'; + +class CustomReporter implements Reporter { + onEnd(config: FullConfig, result: any) { + const outputPath = 'tests/output/latest-result.txt'; + + const allTests = result.suites || []; + + const failed = result.status === 'failed' || result.numFailedTests > 0; + + const date = new Date().toISOString(); + const duration = (result.duration || 0) + 'ms'; + + if (!fs.existsSync('tests/output')) fs.mkdirSync('tests/output', { recursive: true }); + + if (!failed) { + const content = [ + '==================================', + 'TEST RESULT', + 'PASSED', + `Date: ${date}`, + `Total Test: ${result.total || 0}`, + `Passed: ${result.passed || 0}`, + `Failed: ${result.failed || 0}`, + `Duration: ${duration}`, + '==================================', + ].join('\n'); + + fs.writeFileSync(outputPath, content, 'utf-8'); + return; + } + + // Failed: build detailed report + const lines: string[] = []; + lines.push('=================================='); + lines.push('FAILED'); + lines.push(`Date: ${date}`); + lines.push(`Duration: ${duration}`); + lines.push('=================================='); + + for (const res of result.report || []) { + // older Playwright may not provide report; fallback to result.annotations + } + + // Walk tests + const buildFailureDetails = (suites: any[]) => { + for (const s of suites) { + if (s.suites && s.suites.length) buildFailureDetails(s.suites); + if (s.tests && s.tests.length) { + for (const t of s.tests) { + if (t.status === 'failed') { + lines.push('Test Name: ' + t.title.join(' > ')); + // try extract location + const location = t.location ? `${t.location.file}:${t.location.line}` : ''; + if (location) lines.push('Location: ' + location); + for (const r of t.results || []) { + if (r.status === 'failed') { + const error = r.error as TestError | undefined; + lines.push('Error Message: ' + (error?.message || '')); + if (r.stdout && r.stdout.length) lines.push('Stdout: ' + r.stdout.join('\n')); + if (error?.stack) lines.push('Stack Trace: ' + error.stack); + } + } + lines.push('----------------------------------'); + } + } + } + } + }; + + if (result.suites) buildFailureDetails(result.suites); + + fs.writeFileSync(outputPath, lines.join('\n'), 'utf-8'); + } +} + +export default CustomReporter; diff --git a/tests/role-sync.rollback.spec.ts b/tests/role-sync.rollback.spec.ts new file mode 100644 index 0000000..e829b42 --- /dev/null +++ b/tests/role-sync.rollback.spec.ts @@ -0,0 +1,30 @@ +import { RoleSyncService } from '../src/modules/auth/role-sync.service'; + +describe('RoleSyncService - transaction rollback', () => { + let service: RoleSyncService; + const mockPrisma: any = {}; + const mockPermissionCache: any = { invalidate: jest.fn() }; + const mockEvents: any = { publish: jest.fn() }; + const mockGroupHash: any = { compute: (g: any) => require('crypto').createHash('sha256').update((g||[]).slice().sort().join(','), 'utf8').digest('hex') }; + + beforeEach(() => { + mockPrisma.user = { findUnique: jest.fn() }; + mockPrisma.authGroupRoleMapping = { findMany: jest.fn() }; + mockPrisma.userRole = { findMany: jest.fn().mockResolvedValue([]) }; + mockPrisma.user = mockPrisma.user; + // simulate transaction throwing + mockPrisma.$transaction = jest.fn(async (cb: any) => { throw new Error('tx failed'); }); + + service = new RoleSyncService(mockPrisma as any, mockGroupHash as any, mockPermissionCache as any, mockEvents as any); + }); + + test('does not invalidate cache or publish event when transaction fails', async () => { + mockPrisma.user.findUnique.mockResolvedValue({ id: 'uid', lastGroupHash: 'old' }); + mockPrisma.authGroupRoleMapping.findMany.mockResolvedValue([{ roleId: 'r1' }]); + + await expect(service.syncUserRolesFromAuthentik('uid', ['RL-Owner'])).rejects.toThrow('tx failed'); + + expect(mockPermissionCache.invalidate).not.toHaveBeenCalled(); + expect(mockEvents.publish).not.toHaveBeenCalled(); + }); +}); diff --git a/tests/role-sync.service.spec.ts b/tests/role-sync.service.spec.ts new file mode 100644 index 0000000..d08ab89 --- /dev/null +++ b/tests/role-sync.service.spec.ts @@ -0,0 +1,65 @@ +import { RoleSyncService } from '../src/modules/auth/role-sync.service'; + +describe('RoleSyncService', () => { + const mockPrisma: any = {}; + const mockPermissionCache: any = { invalidate: jest.fn() }; + const mockEvents: any = { publish: jest.fn() }; + const mockGroupHash: any = { compute: (g: any) => require('crypto').createHash('sha256').update((g||[]).slice().sort().join(','), 'utf8').digest('hex') }; + let service: RoleSyncService; + + beforeEach(() => { + mockPrisma.user = { findUnique: jest.fn() }; + mockPrisma.authGroupRoleMapping = { findMany: jest.fn() }; + mockPrisma.userRole = { findMany: jest.fn() }; + mockPrisma.auditLog = { create: jest.fn() }; + mockPrisma.user = mockPrisma.user; + mockPrisma.$transaction = jest.fn(async (cb: any) => { + // simulate transaction by calling provided callback with tx = mockPrisma + await cb(mockPrisma); + }); + + service = new RoleSyncService(mockPrisma as any, mockGroupHash as any, mockPermissionCache as any, mockEvents as any); + }); + + test('computeGroupHash consistent and order independent', () => { + const a = ['b', 'a', 'c']; + const h1 = service.computeGroupHash(a); + const h2 = service.computeGroupHash(['a', 'b', 'c']); + expect(h1).toBe(h2); + }); + + test('skips sync when group hash unchanged', async () => { + const groups = ['RL-Owner']; + const hash = service.computeGroupHash(groups); + mockPrisma.user.findUnique.mockResolvedValue({ id: 'uid', lastGroupHash: hash }); + + const res = await service.syncUserRolesFromAuthentik('uid', groups); + expect(res.skipped).toBe(true); + expect(mockPrisma.$transaction).not.toHaveBeenCalled(); + }); + + test('performs transaction and writes audit when changed', async () => { + const groups = ['RL-Owner']; + mockPrisma.user.findUnique.mockResolvedValue({ id: 'uid', lastGroupHash: 'old' }); + mockPrisma.authGroupRoleMapping.findMany.mockResolvedValue([{ roleId: 'r1' }]); + mockPrisma.userRole.findMany.mockResolvedValue([{ roleId: 'r_old' }]); + + // spy on tx ops + mockPrisma.userRole.deleteMany = jest.fn(); + mockPrisma.userRole.createMany = jest.fn(); + mockPrisma.user.update = jest.fn(); + + mockPrisma.auditLog.create = jest.fn(); + + const res = await service.syncUserRolesFromAuthentik('uid', groups); + + expect(mockPrisma.$transaction).toHaveBeenCalled(); + expect(mockPrisma.userRole.deleteMany).toHaveBeenCalledWith({ where: { userId: 'uid', source: 'AUTHENTIK' } }); + expect(mockPrisma.userRole.createMany).toHaveBeenCalled(); + expect(mockPrisma.user.update).toHaveBeenCalled(); + expect(mockPermissionCache.invalidate).toHaveBeenCalledWith('uid'); + expect(mockEvents.publish).toHaveBeenCalledWith(expect.objectContaining({ type: 'RolesSynchronized' })); + expect(res.skipped).toBe(false); + expect(res.assignedRoleIds).toEqual(['r1']); + }); +}); diff --git a/tests/role.handlers.spec.ts b/tests/role.handlers.spec.ts new file mode 100644 index 0000000..2e0a0e8 --- /dev/null +++ b/tests/role.handlers.spec.ts @@ -0,0 +1,41 @@ +import { RoleAssignPermissionHandler } from '../src/modules/identity/application/handlers/role/assign-permission.handler'; +import { RoleRemovePermissionHandler } from '../src/modules/identity/application/handlers/role/remove-permission.handler'; + +describe('Role permission handlers', () => { + test('assign permission invalidates cache and publishes event', async () => { + const mockRoleRepo: any = { findById: jest.fn(), update: jest.fn(), getAssignedUserIds: jest.fn().mockResolvedValue(['u1','u2']) }; + const mockPermRepo: any = { getById: jest.fn() }; + const mockAuthz: any = { invalidateUserPermissions: jest.fn() }; + const mockEvents: any = { publish: jest.fn() }; + + mockRoleRepo.findById.mockResolvedValue({ id: 'r1', assignPermission: jest.fn() }); + mockPermRepo.getById.mockResolvedValue({ id: 'p1' }); + mockRoleRepo.update.mockResolvedValue({ id: 'r1' }); + + const handler = new RoleAssignPermissionHandler(mockRoleRepo, mockPermRepo, mockAuthz as any, mockEvents as any); + + const res = await handler.execute('r1', 'p1'); + + expect(mockRoleRepo.update).toHaveBeenCalled(); + expect(mockAuthz.invalidateUserPermissions).toHaveBeenCalledWith('u1'); + expect(mockAuthz.invalidateUserPermissions).toHaveBeenCalledWith('u2'); + expect(mockEvents.publish).toHaveBeenCalledWith(expect.objectContaining({ type: 'RoleUpdated' })); + }); + + test('remove permission invalidates cache and publishes event', async () => { + const mockRoleRepo: any = { findById: jest.fn(), update: jest.fn(), getAssignedUserIds: jest.fn().mockResolvedValue(['u1']) }; + const mockAuthz: any = { invalidateUserPermissions: jest.fn() }; + const mockEvents: any = { publish: jest.fn() }; + + mockRoleRepo.findById.mockResolvedValue({ id: 'r1', removePermission: jest.fn() }); + mockRoleRepo.update.mockResolvedValue({ id: 'r1' }); + + const handler = new RoleRemovePermissionHandler(mockRoleRepo as any, mockAuthz as any, mockEvents as any); + + const res = await handler.execute('r1', 'p1'); + + expect(mockRoleRepo.update).toHaveBeenCalled(); + expect(mockAuthz.invalidateUserPermissions).toHaveBeenCalledWith('u1'); + expect(mockEvents.publish).toHaveBeenCalledWith(expect.objectContaining({ type: 'RoleUpdated' })); + }); +}); diff --git a/tests/test-artifacts/failed-login-1785604157244-cookies.json b/tests/test-artifacts/failed-login-1785604157244-cookies.json new file mode 100644 index 0000000..0637a08 --- /dev/null +++ b/tests/test-artifacts/failed-login-1785604157244-cookies.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/tests/test-artifacts/failed-login-1785604157244-page.html b/tests/test-artifacts/failed-login-1785604157244-page.html new file mode 100644 index 0000000..fd764ba --- /dev/null +++ b/tests/test-artifacts/failed-login-1785604157244-page.html @@ -0,0 +1 @@ +
{"message":"Cannot GET /","error":"Not Found","statusCode":404}
\ No newline at end of file diff --git a/tests/test-artifacts/failed-login-1785604157244.png b/tests/test-artifacts/failed-login-1785604157244.png new file mode 100644 index 0000000..2a57538 Binary files /dev/null and b/tests/test-artifacts/failed-login-1785604157244.png differ diff --git a/tests/test-artifacts/failed-login-2-1785604169592-cookies.json b/tests/test-artifacts/failed-login-2-1785604169592-cookies.json new file mode 100644 index 0000000..0637a08 --- /dev/null +++ b/tests/test-artifacts/failed-login-2-1785604169592-cookies.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/tests/test-artifacts/failed-login-2-1785604169592-page.html b/tests/test-artifacts/failed-login-2-1785604169592-page.html new file mode 100644 index 0000000..fd764ba --- /dev/null +++ b/tests/test-artifacts/failed-login-2-1785604169592-page.html @@ -0,0 +1 @@ +
{"message":"Cannot GET /","error":"Not Found","statusCode":404}
\ No newline at end of file diff --git a/tests/test-artifacts/failed-login-2-1785604169592.png b/tests/test-artifacts/failed-login-2-1785604169592.png new file mode 100644 index 0000000..2a57538 Binary files /dev/null and b/tests/test-artifacts/failed-login-2-1785604169592.png differ diff --git a/tests/unit/current-user.guard.spec.ts b/tests/unit/current-user.guard.spec.ts new file mode 100644 index 0000000..e7581b4 --- /dev/null +++ b/tests/unit/current-user.guard.spec.ts @@ -0,0 +1,65 @@ +import { CurrentUserGuard } from '../../src/modules/identity/presentation/guards/current-user.guard'; +import { SyncIdentityHandler } from '../../src/modules/identity/application/handlers/user/sync-identity.handler'; +import { IUser } from '../../src/modules/identity/domain/repositories/user.interface'; +import { IdentityData } from '../../src/core/auth/interfaces/identity-data'; +import { UnauthorizedException } from '@nestjs/common'; + +describe('CurrentUserGuard', () => { + let guard: CurrentUserGuard; + let mockSync: Partial; + let mockUserRepo: Partial; + + beforeEach(() => { + mockSync = { execute: jest.fn() }; + mockUserRepo = { getById: jest.fn() }; + guard = new CurrentUserGuard(mockSync as any, mockUserRepo as any); + }); + + function makeContext(req: any): any { + return { switchToHttp: () => ({ getRequest: () => req }) } as any; + } + + test('raylabContext fast path sets currentUser and returns true', async () => { + const user = { id: 'u1' } as any; + const req: any = { raylabContext: { user } }; + const res = await guard.canActivate(makeContext(req)); + expect(res).toBe(true); + expect(req.currentUser).toBe(user); + }); + + test('missing identity throws UnauthorizedException', async () => { + const req: any = {}; + await expect(guard.canActivate(makeContext(req))).rejects.toThrow(UnauthorizedException); + }); + + test('internal identity resolves by getById and does not call sync', async () => { + const identity = new IdentityData('uid', 'u', 'e'); + const req: any = { identity, identitySource: 'internal' }; + (mockUserRepo.getById as jest.Mock).mockResolvedValue({ id: 'uid', roles: [], permissions: [] }); + + const res = await guard.canActivate(makeContext(req)); + expect(res).toBe(true); + expect(req.currentUser).toBeDefined(); + expect((mockSync.execute as jest.Mock)).not.toHaveBeenCalled(); + }); + + test('internal identity with missing user throws UnauthorizedException', async () => { + const identity = new IdentityData('missing', 'u', 'e'); + const req: any = { identity, identitySource: 'internal' }; + (mockUserRepo.getById as jest.Mock).mockRejectedValue(new Error('User tidak ditemukan.')); + + await expect(guard.canActivate(makeContext(req))).rejects.toThrow(UnauthorizedException); + expect((mockSync.execute as jest.Mock)).not.toHaveBeenCalled(); + }); + + test('external identity calls syncIdentityHandler', async () => { + const identity = new IdentityData('extsub', 'u', 'e'); + const req: any = { identity, identitySource: 'external' }; + (mockSync.execute as jest.Mock).mockResolvedValue({ id: 'user-ext' }); + + const res = await guard.canActivate(makeContext(req)); + expect(res).toBe(true); + expect(req.currentUser).toBeDefined(); + expect((mockUserRepo.getById as jest.Mock)).not.toHaveBeenCalled(); + }); +}); diff --git a/tests/unit/debt.service.spec.ts b/tests/unit/debt.service.spec.ts new file mode 100644 index 0000000..b3945ea --- /dev/null +++ b/tests/unit/debt.service.spec.ts @@ -0,0 +1,106 @@ +import { DebtService } from '../../src/modules/bot-debt/application/debt.service'; + +describe('DebtService summary/netting', () => { + let service: DebtService; + let prisma: any; + let mockSync: any; + + beforeEach(async () => { + prisma = { + debtGroupMember: { findUnique: jest.fn().mockResolvedValue({}) }, + debtPerson: { findMany: jest.fn() }, + debtTransaction: { findMany: jest.fn() }, + }; + + mockSync = { execute: jest.fn().mockResolvedValue({ id: 'u' }) }; + + service = new DebtService(prisma as any, mockSync as any); + }); + + function peopleABC() { + return [ + { id: 'A', name: 'A' }, + { id: 'B', name: 'B' }, + { id: 'C', name: 'C' }, + ]; + } + + it('basic debt A->B 17000', async () => { + prisma.debtPerson.findMany.mockResolvedValue(peopleABC()); + prisma.debtTransaction.findMany.mockResolvedValue([ + { fromPersonId: 'A', toPersonId: 'B', amount: BigInt(17000), type: 'DEBT' }, + ]); + + const res = await service.getSummary('u', 'g'); + expect(res).toEqual([{ from: 'A', to: 'B', amount: '17000' }]); + }); + + it('accumulation A->B 17k + 10k = 27k', async () => { + prisma.debtPerson.findMany.mockResolvedValue(peopleABC()); + prisma.debtTransaction.findMany.mockResolvedValue([ + { fromPersonId: 'A', toPersonId: 'B', amount: BigInt(17000), type: 'DEBT' }, + { fromPersonId: 'A', toPersonId: 'B', amount: BigInt(10000), type: 'DEBT' }, + ]); + + const res = await service.getSummary('u', 'g'); + expect(res).toEqual([{ from: 'A', to: 'B', amount: '27000' }]); + }); + + it('reverse debt A->B 27k B->A 5k = A->B 22k', async () => { + prisma.debtPerson.findMany.mockResolvedValue(peopleABC()); + prisma.debtTransaction.findMany.mockResolvedValue([ + { fromPersonId: 'A', toPersonId: 'B', amount: BigInt(27000), type: 'DEBT' }, + { fromPersonId: 'B', toPersonId: 'A', amount: BigInt(5000), type: 'DEBT' }, + ]); + + const res = await service.getSummary('u', 'g'); + expect(res).toEqual([{ from: 'A', to: 'B', amount: '22000' }]); + }); + + it('partial payment A->B 50k debt, payment 20k => A->B 30k', async () => { + prisma.debtPerson.findMany.mockResolvedValue(peopleABC()); + prisma.debtTransaction.findMany.mockResolvedValue([ + { fromPersonId: 'A', toPersonId: 'B', amount: BigInt(50000), type: 'DEBT' }, + { fromPersonId: 'A', toPersonId: 'B', amount: BigInt(20000), type: 'PAYMENT' }, + ]); + + const res = await service.getSummary('u', 'g'); + expect(res).toEqual([{ from: 'A', to: 'B', amount: '30000' }]); + }); + + it('overpayment A->B 50k debt, payment 60k => B->A 10k', async () => { + prisma.debtPerson.findMany.mockResolvedValue(peopleABC()); + prisma.debtTransaction.findMany.mockResolvedValue([ + { fromPersonId: 'A', toPersonId: 'B', amount: BigInt(50000), type: 'DEBT' }, + { fromPersonId: 'A', toPersonId: 'B', amount: BigInt(60000), type: 'PAYMENT' }, + ]); + + const res = await service.getSummary('u', 'g'); + expect(res).toEqual([{ from: 'B', to: 'A', amount: '10000' }]); + }); + + it('cross substitution A->B 100k B->C 100k C->A 100k => empty', async () => { + prisma.debtPerson.findMany.mockResolvedValue(peopleABC()); + prisma.debtTransaction.findMany.mockResolvedValue([ + { fromPersonId: 'A', toPersonId: 'B', amount: BigInt(100000), type: 'DEBT' }, + { fromPersonId: 'B', toPersonId: 'C', amount: BigInt(100000), type: 'DEBT' }, + { fromPersonId: 'C', toPersonId: 'A', amount: BigInt(100000), type: 'DEBT' }, + ]); + + const res = await service.getSummary('u', 'g'); + expect(res).toEqual([]); + }); + + it('cross substitution partial A->B 100k B->C 50k => settlement equivalent', async () => { + prisma.debtPerson.findMany.mockResolvedValue(peopleABC()); + prisma.debtTransaction.findMany.mockResolvedValue([ + { fromPersonId: 'A', toPersonId: 'B', amount: BigInt(100000), type: 'DEBT' }, + { fromPersonId: 'B', toPersonId: 'C', amount: BigInt(50000), type: 'DEBT' }, + ]); + + const res = await service.getSummary('u', 'g'); + // possible valid settlements: A->B 50000, A->C 50000 or other equivalent + expect(res.reduce((acc: any, cur: any) => acc + cur.amount, '')).toBeDefined(); + expect(res.length).toBeGreaterThan(0); + }); +}); diff --git a/tests/unit/jwt-auth.guard.spec.ts b/tests/unit/jwt-auth.guard.spec.ts new file mode 100644 index 0000000..b1a65be --- /dev/null +++ b/tests/unit/jwt-auth.guard.spec.ts @@ -0,0 +1,69 @@ +// Mock 'jose' module before importing JwtAuthGuard so Jest doesn't try to parse ESM from node_modules +let mockJwtVerify: (token: any) => Promise = async () => { throw new Error('no jwks') }; + +jest.mock('jose', () => ({ + createRemoteJWKSet: () => ({}), + jwtVerify: async (token: any, jwks: any, opts: any) => mockJwtVerify(token), +})); + +import { JwtAuthGuard } from '../../src/core/auth/guards/jwt-auth.guard'; +import { ExecutionContext } from '@nestjs/common'; +import * as jwt from 'jsonwebtoken'; + +// These tests focus on identitySource being set based on verification path. + +describe('JwtAuthGuard', () => { + let guard: JwtAuthGuard; + + beforeEach(() => { + guard = new JwtAuthGuard(); + process.env.RAYLAB_JWT_SECRET = 'test-secret'; + delete process.env.AUTHENTIK_JWKS_URI; // ensure JWKS not attempted unless test sets it + // reset mock behavior + mockJwtVerify = async () => { throw new Error('no jwks') }; + }); + + function makeCtxWithAuthHeader(token: string) { + const req: any = { headers: { authorization: `Bearer ${token}` }, cookies: {} }; + const ctx: any = { switchToHttp: () => ({ getRequest: () => req }) } as ExecutionContext; + return { ctx, req }; + } + + test('internal verification sets identitySource=internal', async () => { + const token = jwt.sign({ sub: 'uid', preferred_username: 'u', email: 'e' }, process.env.RAYLAB_JWT_SECRET || 'test-secret'); + const { ctx, req } = makeCtxWithAuthHeader(token); + + const res = await guard.canActivate(ctx); + expect(res).toBe(true); + expect(req.identity).toBeDefined(); + expect((req.identitySource)).toBe('internal'); + }); + + test('external verification sets identitySource=external if JWKS verifies', async () => { + process.env.AUTHENTIK_JWKS_URI = 'https://example.com/.well-known/jwks.json'; + // set mock to succeed + mockJwtVerify = async (token: any) => ({ payload: { sub: 'extsub', preferred_username: 'eu', email: 'ee' } }); + + const token = 'dummy'; + const { ctx, req } = makeCtxWithAuthHeader(token); + + const res = await guard.canActivate(ctx); + expect(res).toBe(true); + expect(req.identity).toBeDefined(); + expect((req.identitySource)).toBe('external'); + }); + + test('external verification fails then internal succeeds -> identitySource=internal', async () => { + process.env.AUTHENTIK_JWKS_URI = 'https://example.com/.well-known/jwks.json'; + // make jwks fail + mockJwtVerify = async (token: any) => { throw new Error('jwks fail'); }; + + const token = jwt.sign({ sub: 'uid2', preferred_username: 'u2', email: 'e2' }, process.env.RAYLAB_JWT_SECRET || 'test-secret'); + const { ctx, req } = makeCtxWithAuthHeader(token); + + const res = await guard.canActivate(ctx); + expect(res).toBe(true); + expect(req.identity).toBeDefined(); + expect((req.identitySource)).toBe('internal'); + }); +}); diff --git a/tests/unit/user.service.spec.ts b/tests/unit/user.service.spec.ts new file mode 100644 index 0000000..d8ad0a1 --- /dev/null +++ b/tests/unit/user.service.spec.ts @@ -0,0 +1,41 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { UserService } from '../../src/modules/identity/application/services/user.service'; +import { PrismaService } from '../../src/shared/prisma.service'; +import { IUser } from '../../src/modules/identity/domain/repositories/user.interface'; + +describe('UserService', () => { + let service: UserService; + let prisma: Partial; + let userRepo: Partial; + + beforeEach(async () => { + prisma = { + user: { + update: jest.fn().mockResolvedValue(null), + findMany: jest.fn().mockResolvedValue([]), + }, + } as any; + + userRepo = { + create: jest.fn().mockImplementation(async (user) => { + return { ...user, id: 'user-1' }; + }), + getById: jest.fn().mockResolvedValue({ id: 'user-1', username: 'u', email: 'e' }), + existByEmail: jest.fn().mockResolvedValue(false), + } as any; + + const module: TestingModule = await Test.createTestingModule({ + providers: [ + UserService, + { provide: PrismaService, useValue: prisma }, + { provide: IUser, useValue: userRepo }, + ], + }).compile(); + + service = module.get(UserService); + }); + + it('createUser - provisioning disabled', async () => { + await expect(service.createUser({ username: 'u', email: 'e' })).rejects.toThrow(); + }); +}); diff --git a/tests/update-user.handler.spec.ts b/tests/update-user.handler.spec.ts new file mode 100644 index 0000000..c1dfb39 --- /dev/null +++ b/tests/update-user.handler.spec.ts @@ -0,0 +1,29 @@ +import { UpdateUserHandler } from '../src/modules/identity/application/handlers/user/update-user.handler'; + +describe('UpdateUserHandler', () => { + test('rejects password in DTO', async () => { + const mockRepo: any = { getById: jest.fn(), update: jest.fn() }; + mockRepo.getById.mockResolvedValue({ id: 'u1', changeUsername: jest.fn(), changeEmail: jest.fn(), setStorageQuota: jest.fn(), setStorageUsed: jest.fn() }); + + const handler = new UpdateUserHandler(mockRepo as any); + + await expect(handler.execute('u1', { password: 'secret' } as any)).rejects.toThrow('Password management is not allowed.'); + }); + + test('updates allowed fields', async () => { + const userObj: any = { id: 'u1', changeUsername: jest.fn(), changeEmail: jest.fn(), setStorageQuota: jest.fn(), setStorageUsed: jest.fn() }; + const mockRepo: any = { getById: jest.fn().mockResolvedValue(userObj), update: jest.fn().mockResolvedValue(userObj) }; + + const handler = new UpdateUserHandler(mockRepo as any); + + const dto = { name: 'New Name', email: 'a@b.com', storageQuota: 1000, storageUsed: 10 } as any; + + const res = await handler.execute('u1', dto); + + expect(userObj.changeUsername).toHaveBeenCalledWith('New Name'); + expect(userObj.changeEmail).toHaveBeenCalledWith('a@b.com'); + expect(userObj.setStorageQuota).toHaveBeenCalledWith(1000); + expect(userObj.setStorageUsed).toHaveBeenCalledWith(10); + expect(mockRepo.update).toHaveBeenCalledWith(userObj); + }); +});