From caec5a618d2780b4060c56771b2f398bc45f1bad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B9=9B=E5=85=AE?= Date: Thu, 11 Jun 2026 20:49:59 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=88=9D=E5=A7=8B=E5=8C=96DevOps?= =?UTF-8?q?=E5=B9=B3=E5=8F=B0=E5=90=8E=E7=AB=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.example | 41 + .gitignore | 8 + .prettierrc | 5 + AGENTS.md | 38 + README.md | 73 + RTK.md | 47 + eslint.config.mjs | 33 + nest-cli.json | 8 + package.json | 67 + pnpm-lock.yaml | 6319 +++++++++++++++++ pnpm-workspace.yaml | 2 + prisma/migrations/README.md | 12 + prisma/schema.prisma | 255 + src/agent/agent.controller.ts | 37 + src/agent/agent.module.ts | 13 + src/agent/agent.service.spec.ts | 103 + src/agent/agent.service.ts | 147 + src/agent/agent.types.ts | 32 + src/agent/create-agent-invocation.dto.ts | 58 + src/agent/llm.client.ts | 75 + src/app.module.ts | 39 + src/audit/audit.controller.ts | 15 + src/audit/audit.module.ts | 11 + src/audit/audit.service.spec.ts | 27 + src/audit/audit.service.ts | 36 + src/common/errors/app-error.ts | 18 + src/common/errors/integration-error.ts | 25 + src/common/http/all-exceptions.filter.ts | 82 + src/common/http/api-envelope.interceptor.ts | 66 + src/common/http/request-id.ts | 25 + src/common/pipes/zod-validation.pipe.ts | 32 + src/common/security/redact-sensitive.ts | 31 + src/config/env.schema.ts | 76 + src/deploy-runs/create-deploy-run.dto.ts | 45 + .../deploy-execution.service.spec.ts | 251 + src/deploy-runs/deploy-execution.service.ts | 541 ++ src/deploy-runs/deploy-run.repository.ts | 341 + src/deploy-runs/deploy-run.seed.ts | 60 + src/deploy-runs/deploy-run.types.ts | 63 + src/deploy-runs/deploy-runs.controller.ts | 69 + src/deploy-runs/deploy-runs.module.ts | 17 + src/deploy-runs/deploy-runs.service.spec.ts | 250 + src/deploy-runs/deploy-runs.service.ts | 198 + src/health/health.controller.ts | 15 + src/health/health.module.ts | 14 + src/health/health.service.spec.ts | 50 + src/health/health.service.ts | 48 + src/integrations/gitea/gitea.client.ts | 105 + src/integrations/gitea/gitea.module.ts | 8 + src/integrations/gitea/gitea.types.ts | 16 + src/integrations/jenkins/jenkins.client.ts | 139 + src/integrations/jenkins/jenkins.module.ts | 8 + src/integrations/jenkins/jenkins.types.ts | 19 + src/main.ts | 35 + .../wecom/deploy-notification.service.spec.ts | 58 + .../wecom/deploy-notification.service.ts | 103 + src/notifications/wecom/wecom.client.ts | 49 + src/notifications/wecom/wecom.module.ts | 9 + src/notifications/wecom/wecom.types.ts | 9 + src/prisma/prisma.module.ts | 9 + src/prisma/prisma.service.ts | 51 + .../process-definition.types.ts | 9 + .../process-definitions.controller.ts | 18 + .../process-definitions.module.ts | 10 + .../process-definitions.service.spec.ts | 22 + .../process-definitions.service.ts | 21 + .../release-process.bpmn.ts | 86 + src/projects/project.repository.ts | 60 + src/projects/project.seed.ts | 114 + src/projects/project.types.ts | 27 + src/projects/projects.controller.ts | 28 + src/projects/projects.module.ts | 13 + src/projects/projects.service.spec.ts | 84 + src/projects/projects.service.ts | 86 + src/settings/settings.controller.ts | 19 + src/settings/settings.module.ts | 9 + src/settings/settings.service.spec.ts | 165 + src/settings/settings.service.ts | 133 + src/settings/settings.types.ts | 27 + src/webhooks/gitea/gitea-signature.spec.ts | 39 + src/webhooks/gitea/gitea-signature.ts | 53 + .../gitea/gitea-webhook.controller.ts | 40 + src/webhooks/gitea/gitea-webhook.module.ts | 12 + .../gitea/gitea-webhook.service.spec.ts | 138 + src/webhooks/gitea/gitea-webhook.service.ts | 177 + src/webhooks/gitea/gitea-webhook.types.ts | 39 + tsconfig.build.json | 4 + tsconfig.json | 24 + 88 files changed, 11893 insertions(+) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 .prettierrc create mode 100644 AGENTS.md create mode 100644 README.md create mode 100644 RTK.md create mode 100644 eslint.config.mjs create mode 100644 nest-cli.json create mode 100644 package.json create mode 100644 pnpm-lock.yaml create mode 100644 pnpm-workspace.yaml create mode 100644 prisma/migrations/README.md create mode 100644 prisma/schema.prisma create mode 100644 src/agent/agent.controller.ts create mode 100644 src/agent/agent.module.ts create mode 100644 src/agent/agent.service.spec.ts create mode 100644 src/agent/agent.service.ts create mode 100644 src/agent/agent.types.ts create mode 100644 src/agent/create-agent-invocation.dto.ts create mode 100644 src/agent/llm.client.ts create mode 100644 src/app.module.ts create mode 100644 src/audit/audit.controller.ts create mode 100644 src/audit/audit.module.ts create mode 100644 src/audit/audit.service.spec.ts create mode 100644 src/audit/audit.service.ts create mode 100644 src/common/errors/app-error.ts create mode 100644 src/common/errors/integration-error.ts create mode 100644 src/common/http/all-exceptions.filter.ts create mode 100644 src/common/http/api-envelope.interceptor.ts create mode 100644 src/common/http/request-id.ts create mode 100644 src/common/pipes/zod-validation.pipe.ts create mode 100644 src/common/security/redact-sensitive.ts create mode 100644 src/config/env.schema.ts create mode 100644 src/deploy-runs/create-deploy-run.dto.ts create mode 100644 src/deploy-runs/deploy-execution.service.spec.ts create mode 100644 src/deploy-runs/deploy-execution.service.ts create mode 100644 src/deploy-runs/deploy-run.repository.ts create mode 100644 src/deploy-runs/deploy-run.seed.ts create mode 100644 src/deploy-runs/deploy-run.types.ts create mode 100644 src/deploy-runs/deploy-runs.controller.ts create mode 100644 src/deploy-runs/deploy-runs.module.ts create mode 100644 src/deploy-runs/deploy-runs.service.spec.ts create mode 100644 src/deploy-runs/deploy-runs.service.ts create mode 100644 src/health/health.controller.ts create mode 100644 src/health/health.module.ts create mode 100644 src/health/health.service.spec.ts create mode 100644 src/health/health.service.ts create mode 100644 src/integrations/gitea/gitea.client.ts create mode 100644 src/integrations/gitea/gitea.module.ts create mode 100644 src/integrations/gitea/gitea.types.ts create mode 100644 src/integrations/jenkins/jenkins.client.ts create mode 100644 src/integrations/jenkins/jenkins.module.ts create mode 100644 src/integrations/jenkins/jenkins.types.ts create mode 100644 src/main.ts create mode 100644 src/notifications/wecom/deploy-notification.service.spec.ts create mode 100644 src/notifications/wecom/deploy-notification.service.ts create mode 100644 src/notifications/wecom/wecom.client.ts create mode 100644 src/notifications/wecom/wecom.module.ts create mode 100644 src/notifications/wecom/wecom.types.ts create mode 100644 src/prisma/prisma.module.ts create mode 100644 src/prisma/prisma.service.ts create mode 100644 src/process-definitions/process-definition.types.ts create mode 100644 src/process-definitions/process-definitions.controller.ts create mode 100644 src/process-definitions/process-definitions.module.ts create mode 100644 src/process-definitions/process-definitions.service.spec.ts create mode 100644 src/process-definitions/process-definitions.service.ts create mode 100644 src/process-definitions/release-process.bpmn.ts create mode 100644 src/projects/project.repository.ts create mode 100644 src/projects/project.seed.ts create mode 100644 src/projects/project.types.ts create mode 100644 src/projects/projects.controller.ts create mode 100644 src/projects/projects.module.ts create mode 100644 src/projects/projects.service.spec.ts create mode 100644 src/projects/projects.service.ts create mode 100644 src/settings/settings.controller.ts create mode 100644 src/settings/settings.module.ts create mode 100644 src/settings/settings.service.spec.ts create mode 100644 src/settings/settings.service.ts create mode 100644 src/settings/settings.types.ts create mode 100644 src/webhooks/gitea/gitea-signature.spec.ts create mode 100644 src/webhooks/gitea/gitea-signature.ts create mode 100644 src/webhooks/gitea/gitea-webhook.controller.ts create mode 100644 src/webhooks/gitea/gitea-webhook.module.ts create mode 100644 src/webhooks/gitea/gitea-webhook.service.spec.ts create mode 100644 src/webhooks/gitea/gitea-webhook.service.ts create mode 100644 src/webhooks/gitea/gitea-webhook.types.ts create mode 100644 tsconfig.build.json create mode 100644 tsconfig.json diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..5dd2654 --- /dev/null +++ b/.env.example @@ -0,0 +1,41 @@ +# Runtime +NODE_ENV=development +PORT=4300 +CORS_ORIGIN=http://localhost:4301 +LOG_LEVEL=info + +# Database +# Do not commit real credentials. Use a local-only .env file or deployment secret manager. +DATABASE_URL=mysql://devops_user:devops_password@127.0.0.1:3306/devops_platform +USE_DATABASE_READS=false +PRISMA_CONNECT_ON_BOOT=false + +# Redis and queues, reserved for async polling/notification workers. +REDIS_URL=redis://127.0.0.1:6379/0 + +# Jenkins integration, server-side only. +JENKINS_BASE_URL=https://jenkins.example.internal +JENKINS_USERNAME=devops-bot +JENKINS_API_TOKEN=replace-with-secret-manager-value + +# Gitea integration, server-side only. +GITEA_BASE_URL=https://gitea.example.internal +GITEA_TOKEN=replace-with-secret-manager-value +GITEA_WEBHOOK_SECRET=replace-with-secret-manager-value + +# Public platform URL used in notification links. +DEVOPS_PUBLIC_URL=https://devops.mrzhan.top + +# Notification provider, server-side only. Supported plan: wecom, feishu, generic. +NOTIFICATION_PROVIDER=replace-with-provider +NOTIFICATION_WEBHOOK_URL=https://notification.example.internal/webhook/replace-with-secret-manager-value +FEISHU_WEBHOOK_URL=https://open.feishu.cn/open-apis/bot/v2/hook/replace-with-secret-manager-value +WECOM_WEBHOOK_URL=https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=replace-with-secret-manager-value + +# LLM proxy configuration, server-side only. +LLM_BASE_URL=https://llm.example.internal/v1 +LLM_API_KEY=replace-with-secret-manager-value +LLM_MODEL=replace-with-model-name + +# Secret encryption key placeholder. Production should provide this via secret manager. +SECRET_ENCRYPTION_KEY=replace-with-32-byte-key diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ad2db40 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +node_modules +dist +coverage +.env +.env.local +.env.*.local +*.log +.DS_Store diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..9d06fb8 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,5 @@ +{ + "singleQuote": true, + "trailingComma": "all", + "semi": true +} diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..c216d2a --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,38 @@ +@RTK.md + +# Agent Instructions + +This backend owns the DevOps platform API only. Do not edit sibling projects from this directory. + +## Hard Boundaries + +- Write only inside `devops-platform-api` unless the user explicitly expands the scope. +- Do not write `devops-platform-web`. +- Do not write `devops-platform-docs/devops-platform-implementation-tracker.md`; the coordinating thread owns tracker updates. +- Do not revert unrelated user or worker changes. + +## Backend Standards + +- Use NestJS modules and dependency injection for all business boundaries. +- Keep controllers thin. Validation, orchestration, and audit decisions belong in services. +- Use Prisma as the default data access layer. Business code must not create mysql2 pools or scatter handwritten SQL. +- If native SQL is unavoidable, place it in a Repository, parameterize it, document why Prisma is insufficient, and keep the method narrow. +- Keep external clients behind integration modules: Jenkins, Gitea, notification providers, and LLM. + +## Security Standards + +- Never commit real credentials. +- Redact tokens, webhook URLs, cookies, authorization headers, private keys, and long opaque secret-like strings before logging, auditing, returning errors, or passing content to LLMs. +- Store future persisted secrets encrypted server-side only. +- Frontend must never receive Jenkins/Gitea/notification/LLM secret values. + +## Audit Standards + +- Record actor, resource, action, before/after state summaries, request source, and parameter digests for deploy, cancel, retry, secret change, and agent invocation operations. +- Deploy run status transitions must be idempotent and concurrency-safe when database writes are enabled. + +## Agent Standards + +- Agent features must be tied to DevOps workflows, not general chat. +- Agent calls are read-only unless a future product decision explicitly changes this. +- Save prompt version, purpose, operator, input digest, output digest, token estimate, and sanitized failure summary for every real invocation. diff --git a/README.md b/README.md new file mode 100644 index 0000000..34929d7 --- /dev/null +++ b/README.md @@ -0,0 +1,73 @@ +# DevOps Platform API + +NestJS + TypeScript backend for the DevOps operations platform. The current local MVP provides the enterprise project skeleton, Prisma data model, configuration validation, integration client boundaries, Gitea webhook handling, Jenkins queue/build synchronization, notification provider boundaries, audit logs, BPMN process definitions, and runnable seed-backed APIs. + +## Scope + +- Manage projects, deploy runs, BPMN step state, Jenkins/Gitea integration state, notification delivery, audit records, and agent invocations. +- Keep all external credentials server-side. +- Use Prisma as the default data access layer. Native SQL is allowed only inside Repository methods with a documented reason. + +## Local Setup + +```bash +pnpm install +cp .env.example .env +pnpm prisma:generate +pnpm start:dev +``` + +The API listens on `http://localhost:4300` by default and runs without a local MySQL database when `USE_DATABASE_READS=false`. In that mode `/projects` and `/deploy-runs` use seed-backed repository data while keeping the same service and controller contracts intended for Prisma-backed reads. + +## Useful Commands + +```bash +pnpm lint +pnpm test +pnpm build +pnpm check +pnpm prisma:migrate:dev --name init +``` + +## API + +- `GET /health`: dependency configuration and health summary. +- `GET /settings/integration-config`: Jenkins/Gitea/notification/LLM/Database/Redis/Secret Encryption Key configuration status. The response only exposes environment variable names, never secret values. +- `GET /projects`: configured project list. +- `GET /projects/:key`: one project by key. +- `GET /deploy-runs`: latest deploy runs. +- `POST /deploy-runs`: create a deploy run. Missing Jenkins config uses a local dry-run path; configured Jenkins calls `buildWithParameters` and returns `queued/running`. +- `POST /deploy-runs/:id/sync-jenkins`: synchronize Jenkins queue/build/log status and advance success or failure. +- `POST /deploy-runs/:id/cancel`: cancel a deploy run. +- `POST /deploy-runs/:id/retry`: create a retry deploy run. +- `POST /webhooks/gitea`: accept Gitea push webhooks and trigger test deploy runs. +- `GET /process-definitions/release`: BPMN release process definition. +- `GET /audit-logs`: latest sanitized audit records. +- `GET /agent/invocations` and `POST /agent/invocations`: DevOps-scoped agent invocation records and mock results. +- `GET /docs`: Swagger UI. + +## Data Access Rules + +- Business modules access data through Repository classes, not direct Prisma calls in controllers. +- Prisma owns connection pooling, transactions, migrations, and typed models. +- Deploy run creation, status transition, step transition, and audit writes must use explicit Prisma transactions once DB writes are enabled. +- Idempotency keys are mandatory for webhook callbacks, Jenkins queue/build tracking, and manual deploy retry paths. +- Raw SQL must be parameterized, isolated in a Repository method, covered by tests, and justified in the method comment. + +## Secret Rules + +- Never commit real `.env`, Jenkins tokens, Gitea tokens, notification webhook URLs, LLM keys, private keys, or cookies. +- Persisted secrets belong in the `secrets` table encrypted with a deployment-provided key or in the deployment secret manager. +- Logs, audit records, integration errors, and LLM prompts must pass through redaction before storage or return. + +## Agent Boundary + +The Agent module is not a generic chat entrypoint. It can only support DevOps workflows: + +- release risk summaries, +- Jenkins failure diagnosis, +- runbook Q&A against whitelisted sources, +- release notes drafts, +- incident review drafts. + +Agent calls are read-only. They must not trigger deploys, mutate Jenkins/Gitea, or expose raw secrets. When `LLM_BASE_URL`, `LLM_API_KEY`, and `LLM_MODEL` are configured, the backend proxies DevOps-scoped prompts to `/chat/completions`; otherwise it returns local mock results with the same API shape. diff --git a/RTK.md b/RTK.md new file mode 100644 index 0000000..06975f3 --- /dev/null +++ b/RTK.md @@ -0,0 +1,47 @@ +# Backend RTK + +## Architecture + +- Framework: NestJS + TypeScript. +- Package manager: pnpm. +- Data access: Prisma Client and Prisma migrations. +- Default local behavior: seed/mock data for runnable APIs until MySQL/Jenkins/Gitea credentials are provided. + +## Module Map + +- `config`: environment schema and validation. +- `prisma`: PrismaService and DB health boundary. +- `health`: runtime and dependency summary. +- `projects`: project configuration API and repository. +- `deploy-runs`: deploy run API, status model, BPMN step summary. +- `integrations/jenkins`: Jenkins API client boundary. +- `integrations/gitea`: Gitea API client boundary. +- `notifications/wecom`: current WeCom adapter boundary; notification provider abstraction is kept for future Feishu or generic webhook adapters. +- `agent`: LLM proxy client boundary for DevOps-only workflows. +- `audit`: sanitized audit record boundary. + +## Data Access Rules + +1. Controllers call services. +2. Services call repositories and integration clients. +3. Repositories call Prisma. +4. Raw SQL is exceptional and repository-only. +5. Transactions are required for deploy run writes once persistence is enabled. + +## Environment Rules + +- `.env.example` may contain placeholders only. +- `.env` is local-only and ignored by git. +- Missing external integration configuration should degrade to `not_configured` in health checks, not crash local development. + +## Verification + +Run these before handing off backend changes when dependencies are available: + +```bash +pnpm install +pnpm prisma:generate +pnpm lint +pnpm test +pnpm build +``` diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000..683e2a3 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,33 @@ +import js from '@eslint/js'; +import prettier from 'eslint-config-prettier'; +import tseslint from 'typescript-eslint'; + +export default tseslint.config( + { + ignores: ['dist', 'coverage', 'node_modules', 'prisma/generated'], + }, + js.configs.recommended, + ...tseslint.configs.recommendedTypeChecked, + prettier, + { + languageOptions: { + parserOptions: { + project: './tsconfig.json', + tsconfigRootDir: import.meta.dirname, + }, + }, + rules: { + '@typescript-eslint/no-explicit-any': 'error', + '@typescript-eslint/no-floating-promises': 'error', + '@typescript-eslint/no-misused-promises': [ + 'error', + { + checksVoidReturn: false, + }, + ], + '@typescript-eslint/no-unsafe-assignment': 'off', + '@typescript-eslint/no-unsafe-member-access': 'off', + '@typescript-eslint/no-unsafe-return': 'off' + }, + }, +); diff --git a/nest-cli.json b/nest-cli.json new file mode 100644 index 0000000..f9aa683 --- /dev/null +++ b/nest-cli.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://json.schemastore.org/nest-cli", + "collection": "@nestjs/schematics", + "sourceRoot": "src", + "compilerOptions": { + "deleteOutDir": true + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..62f54c8 --- /dev/null +++ b/package.json @@ -0,0 +1,67 @@ +{ + "name": "devops-platform-api", + "version": "0.1.0", + "private": true, + "description": "DevOps operations platform API for Jenkins, Gitea, WeCom notifications, BPMN deploy state, and agent-assisted operations.", + "license": "UNLICENSED", + "packageManager": "pnpm@9.15.4", + "engines": { + "node": ">=20.11.0", + "pnpm": ">=9.0.0" + }, + "scripts": { + "build": "nest build", + "check": "pnpm lint && pnpm test && pnpm build", + "format": "prettier --write \"src/**/*.ts\" \"prisma/**/*.prisma\" \"*.md\"", + "lint": "eslint \"{src,prisma}/**/*.{ts,js}\"", + "prisma:generate": "prisma generate", + "prisma:migrate:dev": "prisma migrate dev", + "prisma:studio": "prisma studio", + "start": "nest start", + "start:dev": "nest start --watch", + "start:prod": "node dist/main.js", + "test": "jest --passWithNoTests" + }, + "dependencies": { + "@nestjs/common": "^11.1.6", + "@nestjs/config": "^4.0.2", + "@nestjs/core": "^11.1.6", + "@nestjs/platform-express": "^11.1.6", + "@nestjs/swagger": "^11.2.0", + "@prisma/client": "^6.14.0", + "reflect-metadata": "^0.2.2", + "rxjs": "^7.8.2", + "zod": "^4.0.17" + }, + "devDependencies": { + "@eslint/js": "^9.33.0", + "@nestjs/cli": "^11.0.10", + "@nestjs/schematics": "^11.0.7", + "@nestjs/testing": "^11.1.6", + "@types/jest": "^30.0.0", + "@types/node": "^24.3.0", + "eslint": "^9.33.0", + "eslint-config-prettier": "^10.1.8", + "jest": "^30.0.5", + "prettier": "^3.6.2", + "prisma": "^6.14.0", + "ts-jest": "^29.4.1", + "ts-node": "^10.9.2", + "tsconfig-paths": "^4.2.0", + "typescript": "^5.9.2", + "typescript-eslint": "^8.39.1" + }, + "jest": { + "moduleFileExtensions": [ + "js", + "json", + "ts" + ], + "rootDir": "src", + "testRegex": ".*\\.spec\\.ts$", + "transform": { + "^.+\\.(t|j)s$": "ts-jest" + }, + "testEnvironment": "node" + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..ed8f931 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,6319 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@nestjs/common': + specifier: ^11.1.6 + version: 11.1.26(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/config': + specifier: ^4.0.2 + version: 4.0.4(@nestjs/common@11.1.26(reflect-metadata@0.2.2)(rxjs@7.8.2))(rxjs@7.8.2) + '@nestjs/core': + specifier: ^11.1.6 + version: 11.1.26(@nestjs/common@11.1.26(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.26)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/platform-express': + specifier: ^11.1.6 + version: 11.1.26(@nestjs/common@11.1.26(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.26) + '@nestjs/swagger': + specifier: ^11.2.0 + version: 11.4.4(@nestjs/common@11.1.26(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.26)(reflect-metadata@0.2.2) + '@prisma/client': + specifier: ^6.14.0 + version: 6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3) + reflect-metadata: + specifier: ^0.2.2 + version: 0.2.2 + rxjs: + specifier: ^7.8.2 + version: 7.8.2 + zod: + specifier: ^4.0.17 + version: 4.4.3 + devDependencies: + '@eslint/js': + specifier: ^9.33.0 + version: 9.39.4 + '@nestjs/cli': + specifier: ^11.0.10 + version: 11.0.23(@types/node@24.13.2)(prettier@3.8.4) + '@nestjs/schematics': + specifier: ^11.0.7 + version: 11.1.0(chokidar@4.0.3)(prettier@3.8.4)(typescript@5.9.3) + '@nestjs/testing': + specifier: ^11.1.6 + version: 11.1.26(@nestjs/common@11.1.26(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.26)(@nestjs/platform-express@11.1.26) + '@types/jest': + specifier: ^30.0.0 + version: 30.0.0 + '@types/node': + specifier: ^24.3.0 + version: 24.13.2 + eslint: + specifier: ^9.33.0 + version: 9.39.4(jiti@2.7.0) + eslint-config-prettier: + specifier: ^10.1.8 + version: 10.1.8(eslint@9.39.4(jiti@2.7.0)) + jest: + specifier: ^30.0.5 + version: 30.4.2(@types/node@24.13.2)(ts-node@10.9.2(@types/node@24.13.2)(typescript@5.9.3)) + prettier: + specifier: ^3.6.2 + version: 3.8.4 + prisma: + specifier: ^6.14.0 + version: 6.19.3(typescript@5.9.3) + ts-jest: + specifier: ^29.4.1 + version: 29.4.11(@babel/core@7.29.7)(@jest/transform@30.4.1)(@jest/types@30.4.1)(babel-jest@30.4.1(@babel/core@7.29.7))(jest-util@30.4.1)(jest@30.4.2(@types/node@24.13.2)(ts-node@10.9.2(@types/node@24.13.2)(typescript@5.9.3)))(typescript@5.9.3) + ts-node: + specifier: ^10.9.2 + version: 10.9.2(@types/node@24.13.2)(typescript@5.9.3) + tsconfig-paths: + specifier: ^4.2.0 + version: 4.2.0 + typescript: + specifier: ^5.9.2 + version: 5.9.3 + typescript-eslint: + specifier: ^8.39.1 + version: 8.61.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) + +packages: + + '@angular-devkit/core@19.2.24': + resolution: {integrity: sha512-Kd49warf6U/EyWe5BszF/eebN3zQ3bk7tgfEljAw8q/rX95UUtriJubWvp6pgzHfzBA4jwq8f+QiNZB8eBEXPA==} + engines: {node: ^18.19.1 || ^20.11.1 || >=22.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} + peerDependencies: + chokidar: ^4.0.0 + peerDependenciesMeta: + chokidar: + optional: true + + '@angular-devkit/core@19.2.27': + resolution: {integrity: sha512-3amNzoCVSKd7ah6l6lBQL4onwwJvqvam7FMoQBILrxtW5LB5ezh8gMSPuA4zJjKjoRzf9uoWdlzqv/84I52xZA==} + engines: {node: ^18.19.1 || ^20.11.1 || >=22.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} + peerDependencies: + chokidar: ^4.0.0 + peerDependenciesMeta: + chokidar: + optional: true + + '@angular-devkit/schematics-cli@19.2.27': + resolution: {integrity: sha512-wHYH6SVXVykhLzovUHtYor3Nl4SpIiITi7r9DQDaKYUD4hpRBx25W6N9eGuakT9Vd5tV/x6wmvQFWQZQwFB7eA==} + engines: {node: ^18.19.1 || ^20.11.1 || >=22.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} + hasBin: true + + '@angular-devkit/schematics@19.2.24': + resolution: {integrity: sha512-lnw+ZM1Io+cJAkReC0NPDjqObL8NtKzKIkdgEEKC8CUmkhurYhedbicN8Y8NYHgG1uLd2GozW3+/QqPRZaN+Lw==} + engines: {node: ^18.19.1 || ^20.11.1 || >=22.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} + + '@angular-devkit/schematics@19.2.27': + resolution: {integrity: sha512-/PZmyAlb2NGWPikRRuiWLdfHQd8Wrx6lX4HqvTcaDhlU43M3T0ud4PH2T3QDp7BzHYY92xtD8iPxX2asg67G1A==} + engines: {node: ^18.19.1 || ^20.11.1 || >=22.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} + + '@babel/code-frame@7.29.7': + resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.29.7': + resolution: {integrity: sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.29.7': + resolution: {integrity: sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.29.7': + resolution: {integrity: sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.29.7': + resolution: {integrity: sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==} + engines: {node: '>=6.9.0'} + + '@babel/helper-globals@7.29.7': + resolution: {integrity: sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.29.7': + resolution: {integrity: sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.29.7': + resolution: {integrity: sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-plugin-utils@7.29.7': + resolution: {integrity: sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.29.7': + resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.29.7': + resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.29.7': + resolution: {integrity: sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.29.7': + resolution: {integrity: sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.7': + resolution: {integrity: sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-syntax-async-generators@7.8.4': + resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-bigint@7.8.3': + resolution: {integrity: sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-class-properties@7.12.13': + resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-class-static-block@7.14.5': + resolution: {integrity: sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-import-attributes@7.29.7': + resolution: {integrity: sha512-zGYcYfq/WmZ4V+kBIXQon9dSSc8ircGZqw9ZaNhhGj9nZkeBu1jHLBDQqYYi5WA9uawvA2sIMbry2nCFhf5Djg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-import-meta@7.10.4': + resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-json-strings@7.8.3': + resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-jsx@7.29.7': + resolution: {integrity: sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-logical-assignment-operators@7.10.4': + resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3': + resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-numeric-separator@7.10.4': + resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-object-rest-spread@7.8.3': + resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-optional-catch-binding@7.8.3': + resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-optional-chaining@7.8.3': + resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-private-property-in-object@7.14.5': + resolution: {integrity: sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-top-level-await@7.14.5': + resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-typescript@7.29.7': + resolution: {integrity: sha512-ngr+82Sh0xMz25TPCZi+nC2iTzjfCdWS2ONXTp/PtSCHCgaCNBpdMqgvJ2ccdLlClVZ7sisIgB914j/JFe+RZA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/template@7.29.7': + resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.29.7': + resolution: {integrity: sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.29.7': + resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==} + engines: {node: '>=6.9.0'} + + '@bcoe/v8-coverage@0.2.3': + resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} + + '@borewit/text-codec@0.2.2': + resolution: {integrity: sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ==} + + '@colors/colors@1.5.0': + resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} + engines: {node: '>=0.1.90'} + + '@cspotcode/source-map-support@0.8.1': + resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} + engines: {node: '>=12'} + + '@emnapi/core@1.10.0': + resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} + + '@emnapi/runtime@1.10.0': + resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} + + '@emnapi/wasi-threads@1.2.1': + resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} + + '@eslint-community/eslint-utils@4.9.1': + resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/config-array@0.21.2': + resolution: {integrity: sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/config-helpers@0.4.2': + resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/core@0.17.0': + resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/eslintrc@3.3.5': + resolution: {integrity: sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/js@9.39.4': + resolution: {integrity: sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/object-schema@2.1.7': + resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/plugin-kit@0.4.1': + resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@humanfs/core@0.19.2': + resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.8': + resolution: {integrity: sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==} + engines: {node: '>=18.18.0'} + + '@humanfs/types@0.15.0': + resolution: {integrity: sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} + + '@inquirer/ansi@1.0.2': + resolution: {integrity: sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==} + engines: {node: '>=18'} + + '@inquirer/checkbox@4.3.2': + resolution: {integrity: sha512-VXukHf0RR1doGe6Sm4F0Em7SWYLTHSsbGfJdS9Ja2bX5/D5uwVOEjr07cncLROdBvmnvCATYEWlHqYmXv2IlQA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/confirm@5.1.21': + resolution: {integrity: sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/core@10.3.2': + resolution: {integrity: sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/editor@4.2.23': + resolution: {integrity: sha512-aLSROkEwirotxZ1pBaP8tugXRFCxW94gwrQLxXfrZsKkfjOYC1aRvAZuhpJOb5cu4IBTJdsCigUlf2iCOu4ZDQ==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/expand@4.0.23': + resolution: {integrity: sha512-nRzdOyFYnpeYTTR2qFwEVmIWypzdAx/sIkCMeTNTcflFOovfqUk+HcFhQQVBftAh9gmGrpFj6QcGEqrDMDOiew==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/external-editor@1.0.3': + resolution: {integrity: sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/figures@1.0.15': + resolution: {integrity: sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==} + engines: {node: '>=18'} + + '@inquirer/input@4.3.1': + resolution: {integrity: sha512-kN0pAM4yPrLjJ1XJBjDxyfDduXOuQHrBB8aLDMueuwUGn+vNpF7Gq7TvyVxx8u4SHlFFj4trmj+a2cbpG4Jn1g==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/number@3.0.23': + resolution: {integrity: sha512-5Smv0OK7K0KUzUfYUXDXQc9jrf8OHo4ktlEayFlelCjwMXz0299Y8OrI+lj7i4gCBY15UObk76q0QtxjzFcFcg==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/password@4.0.23': + resolution: {integrity: sha512-zREJHjhT5vJBMZX/IUbyI9zVtVfOLiTO66MrF/3GFZYZ7T4YILW5MSkEYHceSii/KtRk+4i3RE7E1CUXA2jHcA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/prompts@7.10.1': + resolution: {integrity: sha512-Dx/y9bCQcXLI5ooQ5KyvA4FTgeo2jYj/7plWfV5Ak5wDPKQZgudKez2ixyfz7tKXzcJciTxqLeK7R9HItwiByg==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/prompts@7.3.2': + resolution: {integrity: sha512-G1ytyOoHh5BphmEBxSwALin3n1KGNYB6yImbICcRQdzXfOGbuJ9Jske/Of5Sebk339NSGGNfUshnzK8YWkTPsQ==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/rawlist@4.1.11': + resolution: {integrity: sha512-+LLQB8XGr3I5LZN/GuAHo+GpDJegQwuPARLChlMICNdwW7OwV2izlCSCxN6cqpL0sMXmbKbFcItJgdQq5EBXTw==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/search@3.2.2': + resolution: {integrity: sha512-p2bvRfENXCZdWF/U2BXvnSI9h+tuA8iNqtUKb9UWbmLYCRQxd8WkvwWvYn+3NgYaNwdUkHytJMGG4MMLucI1kA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/select@4.4.2': + resolution: {integrity: sha512-l4xMuJo55MAe+N7Qr4rX90vypFwCajSakx59qe/tMaC1aEHWLyw68wF4o0A4SLAY4E0nd+Vt+EyskeDIqu1M6w==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/type@3.0.10': + resolution: {integrity: sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@isaacs/cliui@8.0.2': + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + + '@istanbuljs/load-nyc-config@1.1.0': + resolution: {integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==} + engines: {node: '>=8'} + + '@istanbuljs/schema@0.1.6': + resolution: {integrity: sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==} + engines: {node: '>=8'} + + '@jest/console@30.4.1': + resolution: {integrity: sha512-v3bhyxUh9Hgmo5p6hAOXe14/R3ZxZDOsvHleh4B07z3m/x4/ngPUXEm9XwK4sF4u+f+P2ORb0Ge+MgpaqRMVDA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/core@30.4.2': + resolution: {integrity: sha512-TZJA6cPJUFxoWhxaLo8t0VX/MZX2wPWr0uIDvLSHIvN4gu9h02vSzqI2kBADG1ExqQlC+cY09xKMSreivvrChQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + + '@jest/diff-sequences@30.4.0': + resolution: {integrity: sha512-zOpzlfUs45l6u7jm39qr87JCHUDsaeCtvL+kQe/Vn9jSnRB4/5IPXISm0h9I1vZW/o00Kn4UTJ2MOlhnUGwv3g==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/environment@30.4.1': + resolution: {integrity: sha512-AK9yNRqgKxiabqMoe4oW+3/TSSeV8vkdC7BGaxZdU0AFXfOpofTLqdru2GXKZghP3sdgwE9XXpnVwfZ8JnFV4w==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/expect-utils@30.4.1': + resolution: {integrity: sha512-ZBn5CglH8fBsQsvs4VWNzD4aWfUYks+IdOOQU3MEK71ol/BcVm+P+rtb1KpiFBpSWSCE27uOahyyf1vfqOVbcQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/expect@30.4.1': + resolution: {integrity: sha512-ginrj6TMgh2GshLUGCjO94Ptx9HhdZA/I6A9iUfyeLKFtdAjnKzHDgzgP9HYQgbxM1lbXScQ2eUBz2lGeVDPWA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/fake-timers@30.4.1': + resolution: {integrity: sha512-iW5umdmfPeWzehrVhugFQZqCchSCud5S1l2YT0O9ZhjRR0ExclANDZkiSBwzqtnlOn0J1JXvO+HZ6rkuyOVOgQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/get-type@30.1.0': + resolution: {integrity: sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/globals@30.4.1': + resolution: {integrity: sha512-ZbuY4cmXC8DkxYjfvT2DbcHWL2T6vmsMhXCDcmTB2T0y0gaezBI77ufq5ZAIdcRkYZ7NEQEDg1xFeKbxUJ5v5Q==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/pattern@30.4.0': + resolution: {integrity: sha512-RAWn3+f9u8BsHijKJ71uHcFp6vmyEt6VvoWXkl6hKF3qVIuWNmudVjg12DlBPGup/frIl5UcUlH5HfEuvHpEXg==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/reporters@30.4.1': + resolution: {integrity: sha512-/SnkPCzEQpUaBH81kjdEdDdo2WZl5hxw+BmLDGWjRkm8o7XlhjwsU36cqwe5PGBE5WYpBvDzRSdXx9rbGuJtNA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + + '@jest/schemas@30.4.1': + resolution: {integrity: sha512-i6b4qw5qnP8c5FEeBJg/uZQ4ddrkN6Ca8qISJh0pr7a5hfn3h3v5x60BEbOC7OYAGZNMs1LfFLwnW2CuK8F57Q==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/snapshot-utils@30.4.1': + resolution: {integrity: sha512-ObY4ljvQ95mt6iwKtVLetR/4yXiAgl3H4nJxhztr0MTjrN97TwDYrnCp/kF60Ec9HdhkWTHSu+Hg05aXfngpOA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/source-map@30.0.1': + resolution: {integrity: sha512-MIRWMUUR3sdbP36oyNyhbThLHyJ2eEDClPCiHVbrYAe5g3CHRArIVpBw7cdSB5fr+ofSfIb2Tnsw8iEHL0PYQg==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/test-result@30.4.1': + resolution: {integrity: sha512-/ZG7pgEiOmmWkN9TplKbOu4id2N5lh7FHwRwlkgBVAzGdRH+OkkQ8wX/kIxg4zmd3ZQvAL1RwL2yWsvNYYECTw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/test-sequencer@30.4.1': + resolution: {integrity: sha512-PeYE+4td5rKjoRPxztObrXU+H8hsjZfxKMXOcmrr34JerSyB/ROOxbbicz8B7A5j9R9VayDnVPvBmedqCsFCdw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/transform@30.4.1': + resolution: {integrity: sha512-Wz0LyktlTvRefoymh+n64hQ84KNXsRGcwdoZ8CSa0Ea+fgYcHZlnk+hDP7v2MS7il2bQ5uTEIxf4/NNfhMN4KQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/types@30.4.1': + resolution: {integrity: sha512-f1x/vJXIfjOlEmejYpbkbgw1gOqpPECwMvMEtBqe47j7H2Hg8h8w3o3ikhSXq3MI15kg+oQ0exWO0uCtTNJLoQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/source-map@0.3.11': + resolution: {integrity: sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@jridgewell/trace-mapping@0.3.9': + resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + + '@lukeed/csprng@1.1.0': + resolution: {integrity: sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==} + engines: {node: '>=8'} + + '@microsoft/tsdoc@0.16.0': + resolution: {integrity: sha512-xgAyonlVVS+q7Vc7qLW0UrJU7rSFcETRWsqdXZtjzRU8dF+6CkozTK4V4y1LwOX7j8r/vHphjDeMeGI4tNGeGA==} + + '@napi-rs/wasm-runtime@1.1.5': + resolution: {integrity: sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==} + peerDependencies: + '@emnapi/core': ^1.7.1 + '@emnapi/runtime': ^1.7.1 + + '@nestjs/cli@11.0.23': + resolution: {integrity: sha512-2V0Bf5jz0KXhUZk3eJi9GljIyqH04otwsE/mYLbqJR+X0iiYx+6bkNJ2Qz28uHNFj1cpHgimf9xDzHkqarie0g==} + engines: {node: '>= 20.11'} + hasBin: true + peerDependencies: + '@swc/cli': ^0.1.62 || ^0.3.0 || ^0.4.0 || ^0.5.0 || ^0.6.0 || ^0.7.0 || ^0.8.0 + '@swc/core': ^1.3.62 + peerDependenciesMeta: + '@swc/cli': + optional: true + '@swc/core': + optional: true + + '@nestjs/common@11.1.26': + resolution: {integrity: sha512-0VARQyzuGbprvjO+slWq9Jtj1P0jYCSKAUSv9LWFNWD39ZbDzXXM1pMs35kReVXwchra0urMfTQxw4uAOfdSzA==} + peerDependencies: + class-transformer: '>=0.4.1' + class-validator: '>=0.13.2' + reflect-metadata: ^0.1.12 || ^0.2.0 + rxjs: ^7.1.0 + peerDependenciesMeta: + class-transformer: + optional: true + class-validator: + optional: true + + '@nestjs/config@4.0.4': + resolution: {integrity: sha512-CJPjNitr0bAufSEnRe2N+JbnVmMmDoo6hvKCPzXgZoGwJSmp/dZPk9f/RMbuD/+Q1ZJPjwsRpq0vxna++Knwow==} + peerDependencies: + '@nestjs/common': ^10.0.0 || ^11.0.0 + rxjs: ^7.1.0 + + '@nestjs/core@11.1.26': + resolution: {integrity: sha512-K45zUwYpowEsVqm8qNIzsMcl4LJev0MK9zVhDnmym7YRTJ2/caslqVeKYhPRd5+Fh81IkvWUVu6vEo46uZ5mgQ==} + engines: {node: '>= 20'} + peerDependencies: + '@nestjs/common': ^11.0.0 + '@nestjs/microservices': ^11.0.0 + '@nestjs/platform-express': ^11.0.0 + '@nestjs/websockets': ^11.0.0 + 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@2.1.1': + resolution: {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/platform-express@11.1.26': + resolution: {integrity: sha512-MJ5Kwe52Ag4nlIuLK2ekB6TVYu1a22uvDzc0Aq0wIzcLySIz4YK0fMcrDOKGdbGQWpfZtNu1PM3jhlf4hvf6Og==} + peerDependencies: + '@nestjs/common': ^11.0.0 + '@nestjs/core': ^11.0.0 + + '@nestjs/schematics@11.1.0': + resolution: {integrity: sha512-lVxGZ46tcdItFMoXr6vyKWlnOsm1SZm/GUqAEDvy2RL4Q4O+3bkziAhrO7Y8JLssFUUvNFEGqAizI52WAxhjDw==} + peerDependencies: + prettier: ^3.0.0 + typescript: '>=4.8.2' + peerDependenciesMeta: + prettier: + optional: true + + '@nestjs/swagger@11.4.4': + resolution: {integrity: sha512-VaIo1ruV2G7b+f2zPzkBSUNy9a/WQ9sg8TLKhWlrTfg4O6U10M/PA7Xi6XMXadOVhwOqoesijba8jH3i/3adrA==} + peerDependencies: + '@fastify/static': ^8.0.0 || ^9.0.0 + '@nestjs/common': ^11.0.1 + '@nestjs/core': ^11.0.1 + 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 + + '@nestjs/testing@11.1.26': + resolution: {integrity: sha512-PNf/TSIoF525pb3O9bu06S5QFWl4gArngbjI6yRFKZrF7d85vGjleVkgVVbw879Y95EYDzIS3eel6YlaY4TGFw==} + peerDependencies: + '@nestjs/common': ^11.0.0 + '@nestjs/core': ^11.0.0 + '@nestjs/microservices': ^11.0.0 + '@nestjs/platform-express': ^11.0.0 + peerDependenciesMeta: + '@nestjs/microservices': + optional: true + '@nestjs/platform-express': + optional: true + + '@pkgjs/parseargs@0.11.0': + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + + '@pkgr/core@0.3.6': + resolution: {integrity: sha512-SEeaJLb3qBNF/OaXnaR1NmmBbFYk1zC0ZH/52fATcRPLFg/p791YrcyFFy44Bo9sLaGuSuLp5Q6axbb/O+v/RA==} + engines: {node: ^14.18.0 || >=16.0.0} + + '@prisma/client@6.19.3': + resolution: {integrity: sha512-mKq3jQFhjvko5LTJFHGilsuQs+W+T3Gm451NzuTDGQxwCzwXHYnIu2zGkRoW+Exq3Rob7yp2MfzSrdIiZVhrBg==} + engines: {node: '>=18.18'} + peerDependencies: + prisma: '*' + typescript: '>=5.1.0' + peerDependenciesMeta: + prisma: + optional: true + typescript: + optional: true + + '@prisma/config@6.19.3': + resolution: {integrity: sha512-CBPT44BjlQxEt8kiMEauji2WHTDoVBOKl7UlewXmUgBPnr/oPRZC3psci5chJnYmH0ivEIog2OU9PGWoki3DLQ==} + + '@prisma/debug@6.19.3': + resolution: {integrity: sha512-ljkJ+SgpXNktLG0Q/n4JGYCkKf0f8oYLyjImS2I8e2q2WCfdRRtWER062ZV/ixaNP2M2VKlWXVJiGzZaUgbKZw==} + + '@prisma/engines-version@7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7': + resolution: {integrity: sha512-03bgb1VD5gvuumNf+7fVGBzfpJPjmqV423l/WxsWk2cNQ42JD0/SsFBPhN6z8iAvdHs07/7ei77SKu7aZfq8bA==} + + '@prisma/engines@6.19.3': + resolution: {integrity: sha512-RSYxtlYFl5pJ8ZePgMv0lZ9IzVCOdTPOegrs2qcbAEFrBI1G33h6wyC9kjQvo0DnYEhEVY0X4LsuFHXLKQk88g==} + + '@prisma/fetch-engine@6.19.3': + resolution: {integrity: sha512-tKtl/qco9Nt7LU5iKhpultD8O4vMCZcU2CHjNTnRrL1QvSUr5W/GcyFPjNL87GtRrwBc7ubXXD9xy4EvLvt8JA==} + + '@prisma/get-platform@6.19.3': + resolution: {integrity: sha512-xFj1VcJ1N3MKooOQAGO0W5tsd0W2QzIvW7DD7c/8H14Zmp4jseeWAITm+w2LLoLrlhoHdPPh0NMZ8mfL6puoHA==} + + '@scarf/scarf@1.4.0': + resolution: {integrity: sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==} + + '@sinclair/typebox@0.34.49': + resolution: {integrity: sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A==} + + '@sinonjs/commons@3.0.1': + resolution: {integrity: sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==} + + '@sinonjs/fake-timers@15.4.0': + resolution: {integrity: sha512-DsG+8/LscQIQg68J6Ef3dv10u6nVyetYn923s3/sus5eaGfTo1of5WMZSLf0UJc9KDuKPilPH0UDJCjvNbDNCA==} + + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + + '@tokenizer/inflate@0.4.1': + resolution: {integrity: sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==} + engines: {node: '>=18'} + + '@tokenizer/token@0.3.0': + resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==} + + '@tsconfig/node10@1.0.12': + resolution: {integrity: sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==} + + '@tsconfig/node12@1.0.11': + resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==} + + '@tsconfig/node14@1.0.3': + resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==} + + '@tsconfig/node16@1.0.4': + resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} + + '@tybys/wasm-util@0.10.2': + resolution: {integrity: sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==} + + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.27.0': + resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.28.0': + resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + + '@types/eslint-scope@3.7.7': + resolution: {integrity: sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==} + + '@types/eslint@9.6.1': + resolution: {integrity: sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@types/istanbul-lib-coverage@2.0.6': + resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==} + + '@types/istanbul-lib-report@3.0.3': + resolution: {integrity: sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==} + + '@types/istanbul-reports@3.0.4': + resolution: {integrity: sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==} + + '@types/jest@30.0.0': + resolution: {integrity: sha512-XTYugzhuwqWjws0CVz8QpM36+T+Dz5mTEBKhNs/esGLnCIlGdRy+Dq78NRjd7ls7r8BC8ZRMOrKlkO1hU0JOwA==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/node@24.13.2': + resolution: {integrity: sha512-fRa09kZTgu8o71KFcDjUFuc7F+dEbZYZmkI0mg5YBTRs0yMKjYHsq/c0urDKeDb+D5qVgXOdFcuu+DZPKOITwA==} + + '@types/stack-utils@2.0.3': + resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} + + '@types/yargs-parser@21.0.3': + resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} + + '@types/yargs@17.0.35': + resolution: {integrity: sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==} + + '@typescript-eslint/eslint-plugin@8.61.0': + resolution: {integrity: sha512-bFNvl9ZczlVb+wR2Akszf3gHfKVj/8WanXaGJ3UstTA7brNKg0cNdk6X1Psu5V7MZ2oQtzZKOEzIUehaoxbDGw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.61.0 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/parser@8.61.0': + resolution: {integrity: sha512-5B7PfA2e1NQGCnDHd/0lW7W3gvp3d59Ryw54FYO8Uswxo9f6ikw3AZV+Xj/TvpImmpsiYyUqAfhC6kJID1jF6w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/project-service@8.61.0': + resolution: {integrity: sha512-DV42F7MLJO6Rax7SK1yg43tcnEfGUrurSpSxKuVX+a3RCTzBlH3fuxprrOJXKCJGAaw82xXocikJ0uQaqwXgGA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/scope-manager@8.61.0': + resolution: {integrity: sha512-IWdXFHFSb6mlC3HPc7QsLDm5zYEbUla6trDEHf32D3/dnuUyXd87plScSNXSbm0/RxMvObpI17sv/EDTGrGZkA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.61.0': + resolution: {integrity: sha512-O5Amvdv9ztMpxpf+vmFULGG78IE6Qwdr3bCGvqwG4nwc9H2qXkOYJJnRbRHyMkQTjv1d03olqwwwzHLMqpFePQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/type-utils@8.61.0': + resolution: {integrity: sha512-TuBiQYIkd97yBfInHCTKVYMbX4kvEmpOEuixIuzCU9p8BGT1SfyyO0d0IfDMbPIHcjn/hWnusUX5e8v5Xg+X8A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/types@8.61.0': + resolution: {integrity: sha512-9QTQpZ5Iin4CdIodfbDQFSeiSJKidgYJYug1P9CC2xWgUTvlmixViqDZNciMjwLBZyJnG4tGmPl97rVAFb1AJg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.61.0': + resolution: {integrity: sha512-42zatd5qSvvcV1JdDBCLxYRznvP4eIHpPoZXdkPFnAmanA4FuZ5dibSnCBggY8hQnqajPpoGjXFdZ7fIJKQnlA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/utils@8.61.0': + resolution: {integrity: sha512-3bzFt7ImFMW/jVYwJamDoe/dMOdFLSC6pom6rRjdh4SZJEYupyMzem8e7vKZLclLfpHjlwSAXOUxtKxGXUiLqA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/visitor-keys@8.61.0': + resolution: {integrity: sha512-QVLZu3ZPQEE+HICQyAMZ2yLQhxf0meY/wx6Hx14YcTNj13JB3qHlX3lJ02L3fLGHgERRH71kvYDwiXIguT3AjQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@ungap/structured-clone@1.3.1': + resolution: {integrity: sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ==} + + '@unrs/resolver-binding-android-arm-eabi@1.12.2': + resolution: {integrity: sha512-g5T90pqg1bo/7mytQx6F4iBNC0Wsh9cu+z9veDbFjc7HjpesJFWD7QMS0NGStXM075+7dJPPVvBbpZlnrdpi/w==} + cpu: [arm] + os: [android] + + '@unrs/resolver-binding-android-arm64@1.12.2': + resolution: {integrity: sha512-YGCRZv/9GLhwmz6mYDeTsm/92BAyR28l6c2ReweVW5pWgfsitWLY8upvfRlGdoyD8HjeTHSYJWyZGD4KJA/nFQ==} + cpu: [arm64] + os: [android] + + '@unrs/resolver-binding-darwin-arm64@1.12.2': + resolution: {integrity: sha512-u9DiNT1auQMO20A9SyTuG3wUgQWB9Z7KjAg0uFuCDR1FsAY8A0CG2S6JpHS1xwm/w1G08bjXZDcyOCjv1WAm2w==} + cpu: [arm64] + os: [darwin] + + '@unrs/resolver-binding-darwin-x64@1.12.2': + resolution: {integrity: sha512-f7rPLi/T1HVKZu/u6t87lroib16n8vrSzcyxI7lg4BGO9UF26KhQL44sd9eOUgrTYhvRXtWOIZT5PejdPyJfUA==} + cpu: [x64] + os: [darwin] + + '@unrs/resolver-binding-freebsd-x64@1.12.2': + resolution: {integrity: sha512-BpcOjWCJub6nRZUS2zA20pmLvjtqAtGejETaIyRLiZiQf++cbrjltLA5NN/xaXfqeOBOSlMFbemIl5/S5tljmg==} + cpu: [x64] + os: [freebsd] + + '@unrs/resolver-binding-linux-arm-gnueabihf@1.12.2': + resolution: {integrity: sha512-vZTDvdSISZjJx66OzJqtsOhzifbqRjbmI1Mnu49fQDwog5GtDI4QidRiEAYbZCRj9C8YZEW+3ZjqsyS9GR4k2A==} + cpu: [arm] + os: [linux] + + '@unrs/resolver-binding-linux-arm-musleabihf@1.12.2': + resolution: {integrity: sha512-BiPI+IrIlwcW4nLLMM21+B1dFPzd55yAVgVGrdgDjNef+ch03GdxrcyaIz8X9SsQirh/kCQ7mviyWlMxdh2D7g==} + cpu: [arm] + os: [linux] + + '@unrs/resolver-binding-linux-arm64-gnu@1.12.2': + resolution: {integrity: sha512-zJc0H99FEPoFfSrNpa91HYfxzfAJCr502oxNK1cfdC9hlaFI43RT+JFCann9JUgZmLzzntChHyn13Sgn9ljHNg==} + cpu: [arm64] + os: [linux] + + '@unrs/resolver-binding-linux-arm64-musl@1.12.2': + resolution: {integrity: sha512-KQ3Lki6l+Pz1k/eBipN41ES+YUK30beLGb9YqcB1O542cyLCNE6GaxrfcY3T6EezmGGk84wb5XyO9loTM9tkcA==} + cpu: [arm64] + os: [linux] + + '@unrs/resolver-binding-linux-loong64-gnu@1.12.2': + resolution: {integrity: sha512-3SJGEh1DborhG6pyxvhPzCT4bbSIVihsvgJc13P1bHG7KLdNDaF9T3gsTwFc7Jw/5Y5/iWOjkEx7Zy0NvCGX3Q==} + cpu: [loong64] + os: [linux] + + '@unrs/resolver-binding-linux-loong64-musl@1.12.2': + resolution: {integrity: sha512-jiuG/Obbel7uw1PwHNFfrkiKhLAF6mnyZ6aWlOAVN9WqKm8v0OFGnciJIHu8+CMvXLQ8AD51LPzAoUfT21D5Ew==} + cpu: [loong64] + os: [linux] + + '@unrs/resolver-binding-linux-ppc64-gnu@1.12.2': + resolution: {integrity: sha512-q7xRvVpmcfeL+LlZg8Pbbo6QaTZwDU5BaGZbwfhkEsXJn3Was8xYfE0RBH266xZt0rM6B7i8xAYIvjthuUIWHg==} + cpu: [ppc64] + os: [linux] + + '@unrs/resolver-binding-linux-riscv64-gnu@1.12.2': + resolution: {integrity: sha512-0CVdx6lcnT3Q9inOH8tsMIOJ6ImndllMjqJHg8RLVdB7Vq4SfkEXl9mCSsVNuNA4MCYycRicCUxPCabVHJRr6A==} + cpu: [riscv64] + os: [linux] + + '@unrs/resolver-binding-linux-riscv64-musl@1.12.2': + resolution: {integrity: sha512-iOwlRo9vnp6R6ohHQS11n0NnfdXx/omhkocmIfaPRpQhKZ+3BDMkkdRVh53qjkFkpPddf+FETA28NwGN7l5l+w==} + cpu: [riscv64] + os: [linux] + + '@unrs/resolver-binding-linux-s390x-gnu@1.12.2': + resolution: {integrity: sha512-HYJtLfXq94q8iZNFT1lknx258wlkkWhZeUXJRqzKBBUJ00CvZ+N33zgbCqimLjsyw5Va6uUxhVa12mI+kaveEw==} + cpu: [s390x] + os: [linux] + + '@unrs/resolver-binding-linux-x64-gnu@1.12.2': + resolution: {integrity: sha512-mPsUhunKKDih5O96Y6enDQyHc1SqBPlY1E/SfMWDM3EdJ95Z9CArPeCVwCCqbP45ljvivdEk8Fxn+SIb1rDAJQ==} + cpu: [x64] + os: [linux] + + '@unrs/resolver-binding-linux-x64-musl@1.12.2': + resolution: {integrity: sha512-azrt6+5ydLd8Vt210AAFis/lZevSfPw93EJRIJG+xPu4WCJ8K0kppCTpMyLPcKT7H15M4Jnt2tMp5bOvCkRC6A==} + cpu: [x64] + os: [linux] + + '@unrs/resolver-binding-openharmony-arm64@1.12.2': + resolution: {integrity: sha512-YZ9hP4O0X9PQb8eO980qmLNGH4zT3I9+SZTdt0Pr0YyuGQhYKoOZkV02VzrzyOZJ5xIJ3UFIenKkUkGg8GjgWQ==} + cpu: [arm64] + os: [openharmony] + + '@unrs/resolver-binding-wasm32-wasi@1.12.2': + resolution: {integrity: sha512-tYFDIkMxSflfEc/h92ZWNsZlHSwgimbNHSO3PL2JWQHfCuC2q316jMyYU9TIWZsFK2bQwyK5VAdYgn8ygPj69A==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@unrs/resolver-binding-win32-arm64-msvc@1.12.2': + resolution: {integrity: sha512-qzNyg3xL0VPQmCaUh+N5jSitce6k+uCBfMDesWRnlULOZaqUkaJ0ybdT+UqlAWJoQjuqfIU/0Ptx9bteN4D82g==} + cpu: [arm64] + os: [win32] + + '@unrs/resolver-binding-win32-ia32-msvc@1.12.2': + resolution: {integrity: sha512-WD9sY00OfpHVGfsnHZoA8jVT+esS/Bg8z8jzxp5BnDCjjwsuKsPQrzswwpFy4J1AUJbXPRfkpcX0mXrzeXW79g==} + cpu: [ia32] + os: [win32] + + '@unrs/resolver-binding-win32-x64-msvc@1.12.2': + resolution: {integrity: sha512-nAB74NfSNKknqQ1RrYj6uz8FcXEomu/MATJZxh/x+BArzN2U3JbOYC0APYzUIGhVY3m5hRxA8VPNdPBoG8txlA==} + cpu: [x64] + os: [win32] + + '@webassemblyjs/ast@1.14.1': + resolution: {integrity: sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==} + + '@webassemblyjs/floating-point-hex-parser@1.13.2': + resolution: {integrity: sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==} + + '@webassemblyjs/helper-api-error@1.13.2': + resolution: {integrity: sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==} + + '@webassemblyjs/helper-buffer@1.14.1': + resolution: {integrity: sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==} + + '@webassemblyjs/helper-numbers@1.13.2': + resolution: {integrity: sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==} + + '@webassemblyjs/helper-wasm-bytecode@1.13.2': + resolution: {integrity: sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==} + + '@webassemblyjs/helper-wasm-section@1.14.1': + resolution: {integrity: sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==} + + '@webassemblyjs/ieee754@1.13.2': + resolution: {integrity: sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==} + + '@webassemblyjs/leb128@1.13.2': + resolution: {integrity: sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==} + + '@webassemblyjs/utf8@1.13.2': + resolution: {integrity: sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==} + + '@webassemblyjs/wasm-edit@1.14.1': + resolution: {integrity: sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==} + + '@webassemblyjs/wasm-gen@1.14.1': + resolution: {integrity: sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==} + + '@webassemblyjs/wasm-opt@1.14.1': + resolution: {integrity: sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==} + + '@webassemblyjs/wasm-parser@1.14.1': + resolution: {integrity: sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==} + + '@webassemblyjs/wast-printer@1.14.1': + resolution: {integrity: sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==} + + '@xtuc/ieee754@1.2.0': + resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==} + + '@xtuc/long@4.2.2': + resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==} + + accepts@2.0.0: + resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} + engines: {node: '>= 0.6'} + + acorn-import-phases@1.0.4: + resolution: {integrity: sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==} + engines: {node: '>=10.13.0'} + peerDependencies: + acorn: ^8.14.0 + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn-walk@8.3.5: + resolution: {integrity: sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==} + engines: {node: '>=0.4.0'} + + acorn@8.16.0: + resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} + engines: {node: '>=0.4.0'} + hasBin: true + + ajv-formats@2.1.1: + resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + + ajv-formats@3.0.1: + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + + ajv-keywords@3.5.2: + resolution: {integrity: sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==} + peerDependencies: + ajv: ^6.9.1 + + ajv-keywords@5.1.0: + resolution: {integrity: sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==} + peerDependencies: + ajv: ^8.8.2 + + ajv@6.15.0: + resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==} + + ajv@8.18.0: + resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==} + + ajv@8.20.0: + resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} + + ansi-colors@4.1.3: + resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} + engines: {node: '>=6'} + + ansi-escapes@4.3.2: + resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} + engines: {node: '>=8'} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.2.2: + resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} + engines: {node: '>=12'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} + engines: {node: '>=12'} + + ansis@4.2.0: + resolution: {integrity: sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==} + engines: {node: '>=14'} + + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + + append-field@1.0.0: + resolution: {integrity: sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==} + + arg@4.1.3: + resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} + + argparse@1.0.10: + resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + array-timsort@1.0.3: + resolution: {integrity: sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==} + + babel-jest@30.4.1: + resolution: {integrity: sha512-fATAbM8piYxkiXQp3RBXmZHxZVNJZAVXXfyeyCN2Tida3+qJ8ea9UxhiJ2y4fLO90ZImKt6k9FlcH2+rLkJGhw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + peerDependencies: + '@babel/core': ^7.11.0 || ^8.0.0-0 + + babel-plugin-istanbul@7.0.1: + resolution: {integrity: sha512-D8Z6Qm8jCvVXtIRkBnqNHX0zJ37rQcFJ9u8WOS6tkYOsRdHBzypCstaxWiu5ZIlqQtviRYbgnRLSoCEvjqcqbA==} + engines: {node: '>=12'} + + babel-plugin-jest-hoist@30.4.0: + resolution: {integrity: sha512-9EdtWM/sSfXLOGLwSn+GS6pIXyBnL07/8gyJlwFXjWy4DxMOyItqyUT29d4lQiS380EZwYlX7/At4PgBS+m2aA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + babel-preset-current-node-syntax@1.2.0: + resolution: {integrity: sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==} + peerDependencies: + '@babel/core': ^7.0.0 || ^8.0.0-0 + + babel-preset-jest@30.4.0: + resolution: {integrity: sha512-lBY4jxsNmCnSiu7kquw8ZC9F4+XLMOKypT3RnNHPvU2Kpd4W0xaPuLr5ZkRyOsvLYAY4yaW1ZwTW4xB7NIiZzg==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + peerDependencies: + '@babel/core': ^7.11.0 || ^8.0.0-beta.1 + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + baseline-browser-mapping@2.10.35: + resolution: {integrity: sha512-honAfLBde0HAFLdNyBEfuuENkF6zR+ozxqxa/2zJKHBe1qzLqyTSeRKpdPEHAP03rlDGyQOPnCSxnVpVqQo9Mg==} + engines: {node: '>=6.0.0'} + hasBin: true + + bl@4.1.0: + resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + + body-parser@2.2.2: + resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} + engines: {node: '>=18'} + + brace-expansion@1.1.15: + resolution: {integrity: sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==} + + brace-expansion@2.1.1: + resolution: {integrity: sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==} + + brace-expansion@5.0.6: + resolution: {integrity: sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==} + engines: {node: 18 || 20 || >=22} + + browserslist@4.28.2: + resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + bs-logger@0.2.6: + resolution: {integrity: sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==} + engines: {node: '>= 6'} + + bser@2.1.1: + resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} + + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + + buffer@5.7.1: + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + + busboy@1.6.0: + resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} + engines: {node: '>=10.16.0'} + + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + + c12@3.1.0: + resolution: {integrity: sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw==} + peerDependencies: + magicast: ^0.3.5 + peerDependenciesMeta: + magicast: + optional: true + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + camelcase@5.3.1: + resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} + engines: {node: '>=6'} + + camelcase@6.3.0: + resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} + engines: {node: '>=10'} + + caniuse-lite@1.0.30001799: + resolution: {integrity: sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw==} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + char-regex@1.0.2: + resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} + engines: {node: '>=10'} + + chardet@2.1.1: + resolution: {integrity: sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==} + + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} + + chrome-trace-event@1.0.4: + resolution: {integrity: sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==} + engines: {node: '>=6.0'} + + ci-info@4.4.0: + resolution: {integrity: sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==} + engines: {node: '>=8'} + + citty@0.1.6: + resolution: {integrity: sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==} + + citty@0.2.2: + resolution: {integrity: sha512-+6vJA3L98yv+IdfKGZHBNiGW5KHn22e/JwID0Strsz8h4S/csAu/OuICwxrg44k5MRiZHWIo8XXuJgQTriRP4w==} + + cjs-module-lexer@2.2.0: + resolution: {integrity: sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==} + + cli-cursor@3.1.0: + resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} + engines: {node: '>=8'} + + cli-spinners@2.9.2: + resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} + engines: {node: '>=6'} + + cli-table3@0.6.5: + resolution: {integrity: sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==} + engines: {node: 10.* || >= 12.*} + + cli-width@4.1.0: + resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} + engines: {node: '>= 12'} + + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + + clone@1.0.4: + resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} + engines: {node: '>=0.8'} + + co@4.6.0: + resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==} + engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} + + collect-v8-coverage@1.0.3: + resolution: {integrity: sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + commander@2.20.3: + resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} + + commander@4.1.1: + resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} + engines: {node: '>= 6'} + + comment-json@5.0.0: + resolution: {integrity: sha512-uiqLcOiVDJtBP8WGkZHEP+FZIhTzP1dxvn59EfoYUi9gqupjrBWVQkO2atDrbnKPwLeotFYDsuNb26uBMqB+hw==} + engines: {node: '>= 6'} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + concat-stream@2.0.0: + resolution: {integrity: sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==} + engines: {'0': node >= 6.0} + + confbox@0.2.4: + resolution: {integrity: sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==} + + consola@3.4.2: + resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} + engines: {node: ^14.18.0 || >=16.10.0} + + content-disposition@1.1.0: + resolution: {integrity: sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==} + engines: {node: '>=18'} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + + content-type@2.0.0: + resolution: {integrity: sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==} + engines: {node: '>=18'} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cookie-signature@1.2.2: + resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} + engines: {node: '>=6.6.0'} + + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + + cors@2.8.6: + resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} + engines: {node: '>= 0.10'} + + cosmiconfig@8.3.6: + resolution: {integrity: sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==} + engines: {node: '>=14'} + peerDependencies: + typescript: '>=4.9.5' + peerDependenciesMeta: + typescript: + optional: true + + create-require@1.1.1: + resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + dedent@1.7.2: + resolution: {integrity: sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==} + peerDependencies: + babel-plugin-macros: ^3.1.0 + peerDependenciesMeta: + babel-plugin-macros: + optional: true + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + deepmerge-ts@7.1.5: + resolution: {integrity: sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==} + engines: {node: '>=16.0.0'} + + deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} + + defaults@1.0.4: + resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} + + defu@6.1.7: + resolution: {integrity: sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==} + + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + + destr@2.0.5: + resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==} + + detect-newline@3.1.0: + resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} + engines: {node: '>=8'} + + diff@4.0.4: + resolution: {integrity: sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==} + engines: {node: '>=0.3.1'} + + dotenv-expand@12.0.3: + resolution: {integrity: sha512-uc47g4b+4k/M/SeaW1y4OApx+mtLWl92l5LMPP0GNXctZqELk+YGgOPIIC5elYmUH4OuoK3JLhuRUYegeySiFA==} + engines: {node: '>=12'} + + dotenv@16.6.1: + resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} + engines: {node: '>=12'} + + dotenv@17.4.1: + resolution: {integrity: sha512-k8DaKGP6r1G30Lx8V4+pCsLzKr8vLmV2paqEj1Y55GdAgJuIqpRp5FfajGF8KtwMxCz9qJc6wUIJnm053d/WCw==} + engines: {node: '>=12'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + + effect@3.21.0: + resolution: {integrity: sha512-PPN80qRokCd1f015IANNhrwOnLO7GrrMQfk4/lnZRE/8j7UPWrNNjPV0uBrZutI/nHzernbW+J0hdqQysHiSnQ==} + + electron-to-chromium@1.5.371: + resolution: {integrity: sha512-e9htk9mAYL6AzmkEhSvVVw7IWGSBJ/Bqdn2eRyRLrj1g6sncN4WbFt5qnILYoCktktr45pyjIrOiRvBThQ808w==} + + emittery@0.13.1: + resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==} + engines: {node: '>=12'} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + + empathic@2.0.0: + resolution: {integrity: sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==} + engines: {node: '>=14'} + + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + + enhanced-resolve@5.23.0: + resolution: {integrity: sha512-yJN/BOOLxcOW2aQgeif9mSnaUB8KtvmMMp56oA1kx1CRfBKbhZm2pJ+NBY+3eOboHxix8lfjWpHE0Ei5U8RbSA==} + engines: {node: '>=10.13.0'} + + error-ex@1.3.4: + resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-module-lexer@2.1.0: + resolution: {integrity: sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==} + + es-object-atoms@1.1.2: + resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==} + engines: {node: '>= 0.4'} + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + + escape-string-regexp@2.0.0: + resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==} + engines: {node: '>=8'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-config-prettier@10.1.8: + resolution: {integrity: sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==} + hasBin: true + peerDependencies: + eslint: '>=7.0.0' + + eslint-scope@5.1.1: + resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} + engines: {node: '>=8.0.0'} + + eslint-scope@8.4.0: + resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@4.2.1: + resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@5.0.1: + resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + eslint@9.39.4: + resolution: {integrity: sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@10.4.0: + resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + + esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@4.3.0: + resolution: {integrity: sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + + events@3.3.0: + resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} + engines: {node: '>=0.8.x'} + + execa@5.1.1: + resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} + engines: {node: '>=10'} + + exit-x@0.2.2: + resolution: {integrity: sha512-+I6B/IkJc1o/2tiURyz/ivu/O0nKNEArIUB5O7zBrlDVJr22SCLH3xTeEry428LvFhRzIA1g8izguxJ/gbNcVQ==} + engines: {node: '>= 0.8.0'} + + expect@30.4.1: + resolution: {integrity: sha512-PMARsyh/JtqC20HoGqlFcIlQAyqUtW4PlI1rup1uhYJtKuwAjbvWi3GQMAn+STdHum/dk8xrKfUM1+5SAwpolA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + express@5.2.1: + resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} + engines: {node: '>= 18'} + + exsolve@1.0.8: + resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==} + + fast-check@3.23.2: + resolution: {integrity: sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==} + engines: {node: '>=8.0.0'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fast-safe-stringify@2.1.1: + resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} + + fast-uri@3.1.2: + resolution: {integrity: sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==} + + fb-watchman@2.0.2: + resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + + file-type@21.3.4: + resolution: {integrity: sha512-Ievi/yy8DS3ygGvT47PjSfdFoX+2isQueoYP1cntFW1JLYAuS4GD7NUPGg4zv2iZfV52uDyk5w5Z0TdpRS6Q1g==} + engines: {node: '>=20'} + + finalhandler@2.1.1: + resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} + engines: {node: '>= 18.0.0'} + + find-up@4.1.0: + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flatted@3.4.2: + resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} + + foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} + engines: {node: '>=14'} + + fork-ts-checker-webpack-plugin@9.1.0: + resolution: {integrity: sha512-mpafl89VFPJmhnJ1ssH+8wmM2b50n+Rew5x42NeI2U78aRWgtkEtGmctp7iT16UjquJTjorEmIfESj3DxdW84Q==} + engines: {node: '>=14.21.3'} + peerDependencies: + typescript: '>3.6.0' + webpack: ^5.11.0 + + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + + fresh@2.0.0: + resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} + engines: {node: '>= 0.8'} + + fs-extra@10.1.0: + resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} + engines: {node: '>=12'} + + fs-monkey@1.1.0: + resolution: {integrity: sha512-QMUezzXWII9EV5aTFXW1UBVUO77wYPpjqIF8/AviUCThNeSYZykpoTixUeaNNBwmCev0AMDWMAni+f8Hxb1IFw==} + + fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-package-type@0.1.0: + resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==} + engines: {node: '>=8.0.0'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + get-stream@6.0.1: + resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} + engines: {node: '>=10'} + + giget@2.0.0: + resolution: {integrity: sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==} + hasBin: true + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + glob-to-regexp@0.4.1: + resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} + + glob@10.5.0: + resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + hasBin: true + + glob@13.0.6: + resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==} + engines: {node: 18 || 20 || >=22} + + glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + + globals@14.0.0: + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + handlebars@4.7.9: + resolution: {integrity: sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==} + engines: {node: '>=0.4.7'} + hasBin: true + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + hasown@2.0.4: + resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} + engines: {node: '>= 0.4'} + + html-escaper@2.0.2: + resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + + http-errors@2.0.1: + resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} + engines: {node: '>= 0.8'} + + human-signals@2.1.0: + resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} + engines: {node: '>=10.17.0'} + + iconv-lite@0.7.2: + resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} + engines: {node: '>=0.10.0'} + + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + import-local@3.2.0: + resolution: {integrity: sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==} + engines: {node: '>=8'} + hasBin: true + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + + is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-generator-fn@2.1.0: + resolution: {integrity: sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==} + engines: {node: '>=6'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-interactive@1.0.0: + resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==} + engines: {node: '>=8'} + + is-promise@4.0.0: + resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + + is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + + is-unicode-supported@0.1.0: + resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} + engines: {node: '>=10'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + istanbul-lib-coverage@3.2.2: + resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} + engines: {node: '>=8'} + + istanbul-lib-instrument@6.0.3: + resolution: {integrity: sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==} + engines: {node: '>=10'} + + istanbul-lib-report@3.0.1: + resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} + engines: {node: '>=10'} + + istanbul-lib-source-maps@5.0.6: + resolution: {integrity: sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==} + engines: {node: '>=10'} + + istanbul-reports@3.2.0: + resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} + engines: {node: '>=8'} + + iterare@1.2.1: + resolution: {integrity: sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q==} + engines: {node: '>=6'} + + jackspeak@3.4.3: + resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + + jest-changed-files@30.4.1: + resolution: {integrity: sha512-IuctmYrxi21iOSOaIXpJWalHyPAsVv0GeBHKDn8C1CA4W5htHn7INL+wdnL4Bo0+olEndvAFkmb++tIQJG+vvg==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-circus@30.4.2: + resolution: {integrity: sha512-rvHH7VlY6LgbJXJTQ87GW62g1FntOtbhh0zT+v04kC+pgL6aBKyYINXxWukCpj3dcIBMw5/XUbtDS9dU9JTXeQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-cli@30.4.2: + resolution: {integrity: sha512-jfA2ocvVHMXS2QijrJ0d31ektP+d/W0T5RpcTX2Pq+3sVqHlsXVCM2+FmwpL+bdY8OfHpIg9xMxLF17Zg0U49Q==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + + jest-config@30.4.2: + resolution: {integrity: sha512-rNHAShJQqQwFNoL0hbf3BphSBOWnpOUAKvidLS/AjNVLPfoj5mSf4jQMfW3cYOs6hXeZC7nF7mDHaBnbxELOzg==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + peerDependencies: + '@types/node': '*' + esbuild-register: '>=3.4.0' + ts-node: '>=9.0.0' + peerDependenciesMeta: + '@types/node': + optional: true + esbuild-register: + optional: true + ts-node: + optional: true + + jest-diff@30.4.1: + resolution: {integrity: sha512-CRpFK0RtLriVDGcPPAnR6HMVI8bSR2jnUIgralhauzYQZIb4RH9AtEInTuQr65LmmGggGcRT6HIASxwqsVsmlA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-docblock@30.4.0: + resolution: {integrity: sha512-ZPMabUZCx5MpbZ2eBYSvZ0J8fvo3dR9oM+eeUpb3aKNQFuS2tu3Duw1TNlMoP8k3WQgKGJuhcMFvwcVuq6T7oA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-each@30.4.1: + resolution: {integrity: sha512-/8MJbH6fuj48TstjrMf+u/pd06Qezz5xOXvZA6442heNOWr8bdeoGZX2d9fCn028CoMgYmroH9//zky5GfyYmA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-environment-node@30.4.1: + resolution: {integrity: sha512-4FZYVOk85hz2AyT6BbarKy9u37g6DbrDyCdFhsnDdXqyrueYQvB+0zO4f/kqLCRD0BsPRXPMNJeQwihKZV8naw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-haste-map@30.4.1: + resolution: {integrity: sha512-rFrcONd8jeFsyw+Z9CrScJgglRf2+NFmNam8dKu7n+SoHqNYT47mn0DdEcVUZJpvh7Iz6/si7f7yUH7GJHVgnw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-leak-detector@30.4.1: + resolution: {integrity: sha512-IpmyiioeHxiWDhesHnUFmOxcTzwCwKpgACgWajtAP+nYQXiY7DakTxB6Bx9JFiRMljr0AX1PvnQdaU1KFoz6NQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-matcher-utils@30.4.1: + resolution: {integrity: sha512-zvYfX5CaeEkFrrLS9suWe9rvJrm9J1Iv3ua8kIBv9GEPzcnsfBf0bob37la7s67fs0nlBC3EuvkOLnXQKxtx4A==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-message-util@30.4.1: + resolution: {integrity: sha512-kwCKIvq0MCW1HzLoGola9Te6JUdzgV0loyKJ3Qghrkz9i5/RRIHsL95BMQc2HBBhlBKC4j22K9p11TGHH8RBpQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-mock@30.4.1: + resolution: {integrity: sha512-/i8SVb8/NSB7RfNi8gfqu8gxLV23KaL5EpAttyb9iz8qWRIqXRLflycz/32wXsYkOnaUlx8NAKnJYtpsmXUmfw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-pnp-resolver@1.2.3: + resolution: {integrity: sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==} + engines: {node: '>=6'} + peerDependencies: + jest-resolve: '*' + peerDependenciesMeta: + jest-resolve: + optional: true + + jest-regex-util@30.4.0: + resolution: {integrity: sha512-mWlvLviKIgIQ8VCuM1xRdD0TWp3zlzionlmDBjuXVBs+VkmXq6FgW9T4Emr7oGz/Rk6feDCGyiugolcQEyp3mg==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-resolve-dependencies@30.4.2: + resolution: {integrity: sha512-gDiVh1I+GxYzz9oXlyw+1wv6VOYX1WYxMOfjsA3iGKePV2oxmbHhwxfkALxNxYy1ciw6APWwkW2zZONwP97aEQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-resolve@30.4.1: + resolution: {integrity: sha512-Zry8Yq/yJcNAZ7dJ5F2heic8AheXvbFZ7XI5V+h28nrYZ7Qoyy4dItq8OodjnYD270mvX+ZudmrNV9cysqhW5Q==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-runner@30.4.2: + resolution: {integrity: sha512-2dw0PslVYXxffXGpLo+Ejad+KcI1Qkjn7f4X4619gf21oCUmL+SPfjqIa/losUem3yEOvfNZe/F1HWUcNpODcg==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-runtime@30.4.2: + resolution: {integrity: sha512-3/5e8iPz2k/VLqlr8DgTftYyLUv8Su3FkCAO2/Od81UsUTpSxOrS6O5x5KkoQwyUjmpYyDJKeyAvg2T2nvpNkQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-snapshot@30.4.1: + resolution: {integrity: sha512-tEOkkfOMppUyeiHwjZswOQ3lcnoTnws/q5FnGIaeIh/jmoU0ZlgMYRR8sTlTj+nNGCoJ0RDq6SfxGxCsyMTPmw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-util@30.4.1: + resolution: {integrity: sha512-vjQb1sACEiv13DKJMDToJpzVW0joCsIQrmbg0fi7CyOOt+g9jTuQl2A216pWRBYhOVt53XbL/2LbMKg1BECWOw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-validate@30.4.1: + resolution: {integrity: sha512-PDWi4SOwLnwqNDfHZjOcsEFyZ4fc/2W2gVL3DEoyqnB6jCQMLRtfBong8s6omIw3lI0HWOus12xfnFmQtjW3fw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-watcher@30.4.1: + resolution: {integrity: sha512-/l9UonmvCwjHH7d2h3iAwIloLc1H0S8mJZ/LNK3i86hqwPAz8otUJjP9MfYtz9Tt77Su5FD2xGjZn8d31IZHlw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-worker@27.5.1: + resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} + engines: {node: '>= 10.13.0'} + + jest-worker@30.4.1: + resolution: {integrity: sha512-SHynN/q/QD++iNyvMdy+WMmbCGk8jIsNcRxycXbWubSOhvo6T+j2afcfUSl+3hYsiBebOTo0cT7c2H7CXugu1g==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest@30.4.2: + resolution: {integrity: sha512-Yi1jqNC/Oq0N4hBgNH/YvBpP1P57QqundgytzYqy3yqAa7NZPNjSoi4SGbRAXDMdBzNE6xBCi5U7RgfrvMEUVQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + + jiti@2.7.0: + resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} + hasBin: true + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@3.14.2: + resolution: {integrity: sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==} + hasBin: true + + js-yaml@4.1.1: + resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} + hasBin: true + + js-yaml@4.2.0: + resolution: {integrity: sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==} + hasBin: true + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + jsonc-parser@3.3.1: + resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==} + + jsonfile@6.2.1: + resolution: {integrity: sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + leven@3.1.0: + resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} + engines: {node: '>=6'} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + load-esm@1.0.3: + resolution: {integrity: sha512-v5xlu8eHD1+6r8EHTg6hfmO97LN8ugKtiXcy5e6oN72iD2r6u0RPfLl6fxM+7Wnh2ZRq15o0russMst44WauPA==} + engines: {node: '>=13.2.0'} + + loader-runner@4.3.2: + resolution: {integrity: sha512-DFEqQ3ihfS9blba08cLfYf1NRAIEm+dDjic073DRDc3/JspI/8wYmtDsHwd3+4hwvdxSK7PGaElfTmm0awWJ4w==} + engines: {node: '>=6.11.5'} + + locate-path@5.0.0: + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash.memoize@4.1.2: + resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + lodash@4.18.1: + resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==} + + log-symbols@4.1.0: + resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} + engines: {node: '>=10'} + + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + + lru-cache@11.5.1: + resolution: {integrity: sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==} + engines: {node: 20 || >=22} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + magic-string@0.30.17: + resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} + + make-dir@4.0.0: + resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} + engines: {node: '>=10'} + + make-error@1.3.6: + resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} + + makeerror@1.0.12: + resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + media-typer@0.3.0: + resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} + engines: {node: '>= 0.6'} + + media-typer@1.1.0: + resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} + engines: {node: '>= 0.8'} + + memfs@3.5.3: + resolution: {integrity: sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==} + engines: {node: '>= 4.0.0'} + + merge-descriptors@2.0.0: + resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} + engines: {node: '>=18'} + + merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + mime-types@3.0.2: + resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} + engines: {node: '>=18'} + + mimic-fn@2.1.0: + resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} + engines: {node: '>=6'} + + minimatch@10.2.5: + resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} + engines: {node: 18 || 20 || >=22} + + minimatch@3.1.5: + resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} + + minimatch@9.0.9: + resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==} + engines: {node: '>=16 || 14 >=14.17'} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + minipass@7.1.3: + resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} + engines: {node: '>=16 || 14 >=14.17'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + multer@2.1.1: + resolution: {integrity: sha512-mo+QTzKlx8R7E5ylSXxWzGoXoZbOsRMpyitcht8By2KHvMbf3tjwosZ/Mu/XYU6UuJ3VZnODIrak5ZrPiPyB6A==} + engines: {node: '>= 10.16.0'} + + mute-stream@2.0.0: + resolution: {integrity: sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==} + engines: {node: ^18.17.0 || >=20.5.0} + + napi-postinstall@0.3.4: + resolution: {integrity: sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==} + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + hasBin: true + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + negotiator@1.0.0: + resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} + engines: {node: '>= 0.6'} + + neo-async@2.6.2: + resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} + + node-abort-controller@3.1.1: + resolution: {integrity: sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==} + + node-emoji@1.11.0: + resolution: {integrity: sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A==} + + node-fetch-native@1.6.7: + resolution: {integrity: sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==} + + node-int64@0.4.0: + resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} + + node-releases@2.0.47: + resolution: {integrity: sha512-Uzmd6LXpouKo8EUK68IjH4+E01w/hXyV3R3g/geCJo+rXLNfh1xucB+LOzYEOQPSiUK3h/xZf0cQGcSsmyL2Og==} + engines: {node: '>=18'} + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + npm-run-path@4.0.1: + resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} + engines: {node: '>=8'} + + nypm@0.6.7: + resolution: {integrity: sha512-s3ds97SD5pd1dULE+tHUk1DrV0cSHOnsfpcdGATJ8JpBo21DoKqN9exTH4/2nhPQNOLomBdTFMicN94S4DrZrQ==} + engines: {node: '>=18'} + hasBin: true + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + ohash@2.0.11: + resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==} + + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + onetime@5.1.2: + resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} + engines: {node: '>=6'} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + ora@5.4.1: + resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==} + engines: {node: '>=10'} + + p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@4.1.0: + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + parse-json@5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} + + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} + + path-scurry@2.0.2: + resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==} + engines: {node: 18 || 20 || >=22} + + path-to-regexp@8.4.2: + resolution: {integrity: sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==} + + path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + perfect-debounce@1.0.0: + resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.2: + resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} + engines: {node: '>=8.6'} + + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} + + pirates@4.0.7: + resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} + engines: {node: '>= 6'} + + pkg-dir@4.2.0: + resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} + engines: {node: '>=8'} + + pkg-types@2.3.1: + resolution: {integrity: sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==} + + pluralize@8.0.0: + resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} + engines: {node: '>=4'} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + prettier@3.8.4: + resolution: {integrity: sha512-N2MylSdi48+5N/6S5j+maeHbUSIzzZ5uOcX5Hm4QpV8Dkb1HFjfAKTKX6yNPJQD9AhcT3ifHNB66tWTTJDi11Q==} + engines: {node: '>=14'} + hasBin: true + + pretty-format@30.4.1: + resolution: {integrity: sha512-K6KiKMHTL4jjX4u3Kir2EW07nRfcqVTXIImx50wbjHQTcZPgg+gjVeNTIT3l3L1Rd4UefxfogquC9J37SoFyyw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + prisma@6.19.3: + resolution: {integrity: sha512-++ZJ0ijLrDJF6hNB4t4uxg2br3fC4H9Yc9tcbjr2fcNFP3rh/SBNrAgjhsqBU4Ght8JPrVofG/ZkXfnSfnYsFg==} + engines: {node: '>=18.18'} + hasBin: true + peerDependencies: + typescript: '>=5.1.0' + peerDependenciesMeta: + typescript: + optional: true + + proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + pure-rand@6.1.0: + resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==} + + pure-rand@7.0.1: + resolution: {integrity: sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==} + + qs@6.15.2: + resolution: {integrity: sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==} + engines: {node: '>=0.6'} + + range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + + raw-body@3.0.2: + resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} + engines: {node: '>= 0.10'} + + rc9@2.1.2: + resolution: {integrity: sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==} + + react-is@18.3.1: + resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} + + react-is@19.2.7: + resolution: {integrity: sha512-kZFnouyVv7eP/Phmrlo9FK+zcAdriZJvzxXHF1Sl1P377WSGe2G/JxVolhTrB/jeV47lKImhNUsijjHAAbcl/A==} + + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + + readdirp@4.1.2: + resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} + engines: {node: '>= 14.18.0'} + + reflect-metadata@0.2.2: + resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==} + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + resolve-cwd@3.0.0: + resolution: {integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==} + engines: {node: '>=8'} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + + restore-cursor@3.1.0: + resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} + engines: {node: '>=8'} + + router@2.2.0: + resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} + engines: {node: '>= 18'} + + rxjs@7.8.1: + resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==} + + rxjs@7.8.2: + resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + schema-utils@3.3.0: + resolution: {integrity: sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==} + engines: {node: '>= 10.13.0'} + + schema-utils@4.3.3: + resolution: {integrity: sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==} + engines: {node: '>= 10.13.0'} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.8.4: + resolution: {integrity: sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==} + engines: {node: '>=10'} + hasBin: true + + send@1.2.1: + resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} + engines: {node: '>= 18'} + + serve-static@2.2.1: + resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} + engines: {node: '>= 18'} + + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + side-channel-list@1.0.1: + resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.1: + resolution: {integrity: sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==} + engines: {node: '>= 0.4'} + + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + slash@3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} + + source-map-support@0.5.13: + resolution: {integrity: sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==} + + source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + source-map@0.7.4: + resolution: {integrity: sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==} + engines: {node: '>= 8'} + + sprintf-js@1.0.3: + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + + stack-utils@2.0.6: + resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} + engines: {node: '>=10'} + + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + + streamsearch@1.1.0: + resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} + engines: {node: '>=10.0.0'} + + string-length@4.0.2: + resolution: {integrity: sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==} + engines: {node: '>=10'} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.2.0: + resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} + engines: {node: '>=12'} + + strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + + strip-bom@4.0.0: + resolution: {integrity: sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==} + engines: {node: '>=8'} + + strip-final-newline@2.0.0: + resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} + engines: {node: '>=6'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + strtok3@10.3.5: + resolution: {integrity: sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA==} + engines: {node: '>=18'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-color@8.1.1: + resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} + engines: {node: '>=10'} + + swagger-ui-dist@5.32.6: + resolution: {integrity: sha512-75ttZNaYCLoFPnozPZcTUU6mS3wKT8l7WLjU5zJSHFeJa23i5vtnze6IiCl4jDMPeQTXVXIgovq4M11NNfQvSA==} + + symbol-observable@4.0.0: + resolution: {integrity: sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==} + engines: {node: '>=0.10'} + + synckit@0.11.13: + resolution: {integrity: sha512-eNRKgb3z66Yp3D2CixVujOUvXLFUTij/zVnV8KRyvFdQwpz7I5DS8UfRkTeLzb64u+dkzDSdelE24izu+zSSUg==} + engines: {node: ^14.18.0 || >=16.0.0} + + tapable@2.3.3: + resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} + engines: {node: '>=6'} + + terser-webpack-plugin@5.6.1: + resolution: {integrity: sha512-201R5j+sJpK8nFWwKVyNfZot8FaJbLZDq5evriVzbV1wDtSXDjRUDRfJzHpAaxFDMEhsZL1QkeqM61wgsS3KaQ==} + engines: {node: '>= 10.13.0'} + peerDependencies: + '@minify-html/node': '*' + '@swc/core': '*' + '@swc/css': '*' + '@swc/html': '*' + clean-css: '*' + cssnano: '*' + csso: '*' + esbuild: '*' + html-minifier-terser: '*' + lightningcss: '*' + postcss: '*' + uglify-js: '*' + webpack: ^5.1.0 + peerDependenciesMeta: + '@minify-html/node': + optional: true + '@swc/core': + optional: true + '@swc/css': + optional: true + '@swc/html': + optional: true + clean-css: + optional: true + cssnano: + optional: true + csso: + optional: true + esbuild: + optional: true + html-minifier-terser: + optional: true + lightningcss: + optional: true + postcss: + optional: true + uglify-js: + optional: true + + terser@5.48.0: + resolution: {integrity: sha512-J/9An6vs9Us6wKRriSFXBWdRZapREHqFzdNUKk0pmu804EMR6dr6winwo7e5JDxN4xahxQsuysyYFwlwj4XN/Q==} + engines: {node: '>=10'} + hasBin: true + + test-exclude@6.0.0: + resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} + engines: {node: '>=8'} + + tinyexec@1.2.4: + resolution: {integrity: sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==} + engines: {node: '>=18'} + + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + + tmpl@1.0.5: + resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} + + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + + token-types@6.1.2: + resolution: {integrity: sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==} + engines: {node: '>=14.16'} + + ts-api-utils@2.5.0: + resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + + ts-jest@29.4.11: + resolution: {integrity: sha512-IrFl7l9AuB/qrNw5quqvAv/hmKMb8dhWOH4jQOGo0Oq8tCeo1O86/iTFG1FaRimgUkF13l4PcepO8ATFT6Ns4g==} + engines: {node: ^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0} + hasBin: true + 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 + esbuild: '*' + 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@10.9.2: + resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} + hasBin: true + 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 + + tsconfig-paths-webpack-plugin@4.2.0: + resolution: {integrity: sha512-zbem3rfRS8BgeNK50Zz5SIQgXzLafiHjOwUAvk/38/o1jHn/V5QAgVUcz884or7WYcPaH3N2CIfUc2u0ul7UcA==} + engines: {node: '>=10.13.0'} + + tsconfig-paths@4.2.0: + resolution: {integrity: sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==} + engines: {node: '>=6'} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + type-detect@4.0.8: + resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} + engines: {node: '>=4'} + + type-fest@0.21.3: + resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} + engines: {node: '>=10'} + + type-fest@4.41.0: + resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} + engines: {node: '>=16'} + + type-is@1.6.18: + resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} + engines: {node: '>= 0.6'} + + type-is@2.1.0: + resolution: {integrity: sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==} + engines: {node: '>= 18'} + + typedarray@0.0.6: + resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} + + typescript-eslint@8.61.0: + resolution: {integrity: sha512-8y31Rd0eGTrDKqhy6vT0HtzhN+YLjQizwX3aA3hPXP/ynSfnrBXcQY5IzsP9/DM7+klX4IUncZZjkchP0z+rUw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + uglify-js@3.19.3: + resolution: {integrity: sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==} + engines: {node: '>=0.8.0'} + hasBin: true + + uid@2.0.2: + resolution: {integrity: sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g==} + engines: {node: '>=8'} + + uint8array-extras@1.5.0: + resolution: {integrity: sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==} + engines: {node: '>=18'} + + undici-types@7.18.2: + resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} + + universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + + unrs-resolver@1.12.2: + resolution: {integrity: sha512-dmlRxBJJayXjqTwC+JtF1HhJmgf3ftQ3YejFcZrf4+KKtJv0qDsK1pjqaaVjG7wJ5NJ6UVP1OqRMQ71Z4C3rxQ==} + + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + v8-compile-cache-lib@3.0.1: + resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} + + v8-to-istanbul@9.3.0: + resolution: {integrity: sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==} + engines: {node: '>=10.12.0'} + + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + + walker@1.0.8: + resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} + + watchpack@2.5.1: + resolution: {integrity: sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==} + engines: {node: '>=10.13.0'} + + wcwidth@1.0.1: + resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} + + webpack-node-externals@3.0.0: + resolution: {integrity: sha512-LnL6Z3GGDPht/AigwRh2dvL9PQPFQ8skEpVrWZXLWBYmqcaojHNN0onvHzie6rq7EWKrrBfPYqNEzTJgiwEQDQ==} + engines: {node: '>=6'} + + webpack-sources@3.5.0: + resolution: {integrity: sha512-HPuy+uuoTCaaoEoI1LQ3JN9+vrPBvEesnnX1jADHy728cHSMlq4wUc4afYqahq2B1mhQVZxCXOkNTnXltr+2vQ==} + engines: {node: '>=10.13.0'} + + webpack@5.106.2: + resolution: {integrity: sha512-wGN3qcrBQIFmQ/c0AiOAQBvrZ5lmY8vbbMv4Mxfgzqd/B6+9pXtLo73WuS1dSGXM5QYY3hZnIbvx+K1xxe6FyA==} + engines: {node: '>=10.13.0'} + hasBin: true + peerDependencies: + webpack-cli: '*' + peerDependenciesMeta: + webpack-cli: + optional: true + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + wordwrap@1.0.0: + resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} + + wrap-ansi@6.2.0: + resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} + engines: {node: '>=8'} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + write-file-atomic@5.0.1: + resolution: {integrity: sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + + yn@3.1.1: + resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} + engines: {node: '>=6'} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + yoctocolors-cjs@2.1.3: + resolution: {integrity: sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==} + engines: {node: '>=18'} + + zod@4.4.3: + resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} + +snapshots: + + '@angular-devkit/core@19.2.24(chokidar@4.0.3)': + dependencies: + ajv: 8.18.0 + ajv-formats: 3.0.1(ajv@8.18.0) + jsonc-parser: 3.3.1 + picomatch: 4.0.4 + rxjs: 7.8.1 + source-map: 0.7.4 + optionalDependencies: + chokidar: 4.0.3 + + '@angular-devkit/core@19.2.27(chokidar@4.0.3)': + dependencies: + ajv: 8.18.0 + ajv-formats: 3.0.1(ajv@8.18.0) + jsonc-parser: 3.3.1 + picomatch: 4.0.4 + rxjs: 7.8.1 + source-map: 0.7.4 + optionalDependencies: + chokidar: 4.0.3 + + '@angular-devkit/schematics-cli@19.2.27(@types/node@24.13.2)(chokidar@4.0.3)': + dependencies: + '@angular-devkit/core': 19.2.27(chokidar@4.0.3) + '@angular-devkit/schematics': 19.2.27(chokidar@4.0.3) + '@inquirer/prompts': 7.3.2(@types/node@24.13.2) + ansi-colors: 4.1.3 + symbol-observable: 4.0.0 + yargs-parser: 21.1.1 + transitivePeerDependencies: + - '@types/node' + - chokidar + + '@angular-devkit/schematics@19.2.24(chokidar@4.0.3)': + dependencies: + '@angular-devkit/core': 19.2.24(chokidar@4.0.3) + jsonc-parser: 3.3.1 + magic-string: 0.30.17 + ora: 5.4.1 + rxjs: 7.8.1 + transitivePeerDependencies: + - chokidar + + '@angular-devkit/schematics@19.2.27(chokidar@4.0.3)': + dependencies: + '@angular-devkit/core': 19.2.27(chokidar@4.0.3) + jsonc-parser: 3.3.1 + magic-string: 0.30.17 + ora: 5.4.1 + rxjs: 7.8.1 + transitivePeerDependencies: + - chokidar + + '@babel/code-frame@7.29.7': + dependencies: + '@babel/helper-validator-identifier': 7.29.7 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.29.7': {} + + '@babel/core@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.7 + '@babel/helper-compilation-targets': 7.29.7 + '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7) + '@babel/helpers': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/template': 7.29.7 + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.29.7': + dependencies: + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-compilation-targets@7.29.7': + dependencies: + '@babel/compat-data': 7.29.7 + '@babel/helper-validator-option': 7.29.7 + browserslist: 4.28.2 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-globals@7.29.7': {} + + '@babel/helper-module-imports@7.29.7': + dependencies: + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-module-imports': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + '@babel/traverse': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/helper-plugin-utils@7.29.7': {} + + '@babel/helper-string-parser@7.29.7': {} + + '@babel/helper-validator-identifier@7.29.7': {} + + '@babel/helper-validator-option@7.29.7': {} + + '@babel/helpers@7.29.7': + dependencies: + '@babel/template': 7.29.7 + '@babel/types': 7.29.7 + + '@babel/parser@7.29.7': + dependencies: + '@babel/types': 7.29.7 + + '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-syntax-import-attributes@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/template@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + + '@babel/traverse@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.7 + '@babel/helper-globals': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/template': 7.29.7 + '@babel/types': 7.29.7 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.29.7': + dependencies: + '@babel/helper-string-parser': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + + '@bcoe/v8-coverage@0.2.3': {} + + '@borewit/text-codec@0.2.2': {} + + '@colors/colors@1.5.0': + optional: true + + '@cspotcode/source-map-support@0.8.1': + dependencies: + '@jridgewell/trace-mapping': 0.3.9 + + '@emnapi/core@1.10.0': + dependencies: + '@emnapi/wasi-threads': 1.2.1 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.10.0': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.2.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@eslint-community/eslint-utils@4.9.1(eslint@9.39.4(jiti@2.7.0))': + dependencies: + eslint: 9.39.4(jiti@2.7.0) + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.2': {} + + '@eslint/config-array@0.21.2': + dependencies: + '@eslint/object-schema': 2.1.7 + debug: 4.4.3 + minimatch: 3.1.5 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.4.2': + dependencies: + '@eslint/core': 0.17.0 + + '@eslint/core@0.17.0': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/eslintrc@3.3.5': + dependencies: + ajv: 6.15.0 + debug: 4.4.3 + espree: 10.4.0 + globals: 14.0.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.2.0 + minimatch: 3.1.5 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@9.39.4': {} + + '@eslint/object-schema@2.1.7': {} + + '@eslint/plugin-kit@0.4.1': + dependencies: + '@eslint/core': 0.17.0 + levn: 0.4.1 + + '@humanfs/core@0.19.2': + dependencies: + '@humanfs/types': 0.15.0 + + '@humanfs/node@0.16.8': + dependencies: + '@humanfs/core': 0.19.2 + '@humanfs/types': 0.15.0 + '@humanwhocodes/retry': 0.4.3 + + '@humanfs/types@0.15.0': {} + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.4.3': {} + + '@inquirer/ansi@1.0.2': {} + + '@inquirer/checkbox@4.3.2(@types/node@24.13.2)': + dependencies: + '@inquirer/ansi': 1.0.2 + '@inquirer/core': 10.3.2(@types/node@24.13.2) + '@inquirer/figures': 1.0.15 + '@inquirer/type': 3.0.10(@types/node@24.13.2) + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 24.13.2 + + '@inquirer/confirm@5.1.21(@types/node@24.13.2)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@24.13.2) + '@inquirer/type': 3.0.10(@types/node@24.13.2) + optionalDependencies: + '@types/node': 24.13.2 + + '@inquirer/core@10.3.2(@types/node@24.13.2)': + dependencies: + '@inquirer/ansi': 1.0.2 + '@inquirer/figures': 1.0.15 + '@inquirer/type': 3.0.10(@types/node@24.13.2) + cli-width: 4.1.0 + mute-stream: 2.0.0 + signal-exit: 4.1.0 + wrap-ansi: 6.2.0 + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 24.13.2 + + '@inquirer/editor@4.2.23(@types/node@24.13.2)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@24.13.2) + '@inquirer/external-editor': 1.0.3(@types/node@24.13.2) + '@inquirer/type': 3.0.10(@types/node@24.13.2) + optionalDependencies: + '@types/node': 24.13.2 + + '@inquirer/expand@4.0.23(@types/node@24.13.2)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@24.13.2) + '@inquirer/type': 3.0.10(@types/node@24.13.2) + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 24.13.2 + + '@inquirer/external-editor@1.0.3(@types/node@24.13.2)': + dependencies: + chardet: 2.1.1 + iconv-lite: 0.7.2 + optionalDependencies: + '@types/node': 24.13.2 + + '@inquirer/figures@1.0.15': {} + + '@inquirer/input@4.3.1(@types/node@24.13.2)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@24.13.2) + '@inquirer/type': 3.0.10(@types/node@24.13.2) + optionalDependencies: + '@types/node': 24.13.2 + + '@inquirer/number@3.0.23(@types/node@24.13.2)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@24.13.2) + '@inquirer/type': 3.0.10(@types/node@24.13.2) + optionalDependencies: + '@types/node': 24.13.2 + + '@inquirer/password@4.0.23(@types/node@24.13.2)': + dependencies: + '@inquirer/ansi': 1.0.2 + '@inquirer/core': 10.3.2(@types/node@24.13.2) + '@inquirer/type': 3.0.10(@types/node@24.13.2) + optionalDependencies: + '@types/node': 24.13.2 + + '@inquirer/prompts@7.10.1(@types/node@24.13.2)': + dependencies: + '@inquirer/checkbox': 4.3.2(@types/node@24.13.2) + '@inquirer/confirm': 5.1.21(@types/node@24.13.2) + '@inquirer/editor': 4.2.23(@types/node@24.13.2) + '@inquirer/expand': 4.0.23(@types/node@24.13.2) + '@inquirer/input': 4.3.1(@types/node@24.13.2) + '@inquirer/number': 3.0.23(@types/node@24.13.2) + '@inquirer/password': 4.0.23(@types/node@24.13.2) + '@inquirer/rawlist': 4.1.11(@types/node@24.13.2) + '@inquirer/search': 3.2.2(@types/node@24.13.2) + '@inquirer/select': 4.4.2(@types/node@24.13.2) + optionalDependencies: + '@types/node': 24.13.2 + + '@inquirer/prompts@7.3.2(@types/node@24.13.2)': + dependencies: + '@inquirer/checkbox': 4.3.2(@types/node@24.13.2) + '@inquirer/confirm': 5.1.21(@types/node@24.13.2) + '@inquirer/editor': 4.2.23(@types/node@24.13.2) + '@inquirer/expand': 4.0.23(@types/node@24.13.2) + '@inquirer/input': 4.3.1(@types/node@24.13.2) + '@inquirer/number': 3.0.23(@types/node@24.13.2) + '@inquirer/password': 4.0.23(@types/node@24.13.2) + '@inquirer/rawlist': 4.1.11(@types/node@24.13.2) + '@inquirer/search': 3.2.2(@types/node@24.13.2) + '@inquirer/select': 4.4.2(@types/node@24.13.2) + optionalDependencies: + '@types/node': 24.13.2 + + '@inquirer/rawlist@4.1.11(@types/node@24.13.2)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@24.13.2) + '@inquirer/type': 3.0.10(@types/node@24.13.2) + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 24.13.2 + + '@inquirer/search@3.2.2(@types/node@24.13.2)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@24.13.2) + '@inquirer/figures': 1.0.15 + '@inquirer/type': 3.0.10(@types/node@24.13.2) + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 24.13.2 + + '@inquirer/select@4.4.2(@types/node@24.13.2)': + dependencies: + '@inquirer/ansi': 1.0.2 + '@inquirer/core': 10.3.2(@types/node@24.13.2) + '@inquirer/figures': 1.0.15 + '@inquirer/type': 3.0.10(@types/node@24.13.2) + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 24.13.2 + + '@inquirer/type@3.0.10(@types/node@24.13.2)': + optionalDependencies: + '@types/node': 24.13.2 + + '@isaacs/cliui@8.0.2': + dependencies: + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.2.0 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 + + '@istanbuljs/load-nyc-config@1.1.0': + dependencies: + camelcase: 5.3.1 + find-up: 4.1.0 + get-package-type: 0.1.0 + js-yaml: 3.14.2 + resolve-from: 5.0.0 + + '@istanbuljs/schema@0.1.6': {} + + '@jest/console@30.4.1': + dependencies: + '@jest/types': 30.4.1 + '@types/node': 24.13.2 + chalk: 4.1.2 + jest-message-util: 30.4.1 + jest-util: 30.4.1 + slash: 3.0.0 + + '@jest/core@30.4.2(ts-node@10.9.2(@types/node@24.13.2)(typescript@5.9.3))': + dependencies: + '@jest/console': 30.4.1 + '@jest/pattern': 30.4.0 + '@jest/reporters': 30.4.1 + '@jest/test-result': 30.4.1 + '@jest/transform': 30.4.1 + '@jest/types': 30.4.1 + '@types/node': 24.13.2 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + ci-info: 4.4.0 + exit-x: 0.2.2 + fast-json-stable-stringify: 2.1.0 + graceful-fs: 4.2.11 + jest-changed-files: 30.4.1 + jest-config: 30.4.2(@types/node@24.13.2)(ts-node@10.9.2(@types/node@24.13.2)(typescript@5.9.3)) + jest-haste-map: 30.4.1 + jest-message-util: 30.4.1 + jest-regex-util: 30.4.0 + jest-resolve: 30.4.1 + jest-resolve-dependencies: 30.4.2 + jest-runner: 30.4.2 + jest-runtime: 30.4.2 + jest-snapshot: 30.4.1 + jest-util: 30.4.1 + jest-validate: 30.4.1 + jest-watcher: 30.4.1 + pretty-format: 30.4.1 + slash: 3.0.0 + transitivePeerDependencies: + - babel-plugin-macros + - esbuild-register + - supports-color + - ts-node + + '@jest/diff-sequences@30.4.0': {} + + '@jest/environment@30.4.1': + dependencies: + '@jest/fake-timers': 30.4.1 + '@jest/types': 30.4.1 + '@types/node': 24.13.2 + jest-mock: 30.4.1 + + '@jest/expect-utils@30.4.1': + dependencies: + '@jest/get-type': 30.1.0 + + '@jest/expect@30.4.1': + dependencies: + expect: 30.4.1 + jest-snapshot: 30.4.1 + transitivePeerDependencies: + - supports-color + + '@jest/fake-timers@30.4.1': + dependencies: + '@jest/types': 30.4.1 + '@sinonjs/fake-timers': 15.4.0 + '@types/node': 24.13.2 + jest-message-util: 30.4.1 + jest-mock: 30.4.1 + jest-util: 30.4.1 + + '@jest/get-type@30.1.0': {} + + '@jest/globals@30.4.1': + dependencies: + '@jest/environment': 30.4.1 + '@jest/expect': 30.4.1 + '@jest/types': 30.4.1 + jest-mock: 30.4.1 + transitivePeerDependencies: + - supports-color + + '@jest/pattern@30.4.0': + dependencies: + '@types/node': 24.13.2 + jest-regex-util: 30.4.0 + + '@jest/reporters@30.4.1': + dependencies: + '@bcoe/v8-coverage': 0.2.3 + '@jest/console': 30.4.1 + '@jest/test-result': 30.4.1 + '@jest/transform': 30.4.1 + '@jest/types': 30.4.1 + '@jridgewell/trace-mapping': 0.3.31 + '@types/node': 24.13.2 + chalk: 4.1.2 + collect-v8-coverage: 1.0.3 + exit-x: 0.2.2 + glob: 10.5.0 + graceful-fs: 4.2.11 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-instrument: 6.0.3 + istanbul-lib-report: 3.0.1 + istanbul-lib-source-maps: 5.0.6 + istanbul-reports: 3.2.0 + jest-message-util: 30.4.1 + jest-util: 30.4.1 + jest-worker: 30.4.1 + slash: 3.0.0 + string-length: 4.0.2 + v8-to-istanbul: 9.3.0 + transitivePeerDependencies: + - supports-color + + '@jest/schemas@30.4.1': + dependencies: + '@sinclair/typebox': 0.34.49 + + '@jest/snapshot-utils@30.4.1': + dependencies: + '@jest/types': 30.4.1 + chalk: 4.1.2 + graceful-fs: 4.2.11 + natural-compare: 1.4.0 + + '@jest/source-map@30.0.1': + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + callsites: 3.1.0 + graceful-fs: 4.2.11 + + '@jest/test-result@30.4.1': + dependencies: + '@jest/console': 30.4.1 + '@jest/types': 30.4.1 + '@types/istanbul-lib-coverage': 2.0.6 + collect-v8-coverage: 1.0.3 + + '@jest/test-sequencer@30.4.1': + dependencies: + '@jest/test-result': 30.4.1 + graceful-fs: 4.2.11 + jest-haste-map: 30.4.1 + slash: 3.0.0 + + '@jest/transform@30.4.1': + dependencies: + '@babel/core': 7.29.7 + '@jest/types': 30.4.1 + '@jridgewell/trace-mapping': 0.3.31 + babel-plugin-istanbul: 7.0.1 + chalk: 4.1.2 + convert-source-map: 2.0.0 + fast-json-stable-stringify: 2.1.0 + graceful-fs: 4.2.11 + jest-haste-map: 30.4.1 + jest-regex-util: 30.4.0 + jest-util: 30.4.1 + pirates: 4.0.7 + slash: 3.0.0 + write-file-atomic: 5.0.1 + transitivePeerDependencies: + - supports-color + + '@jest/types@30.4.1': + dependencies: + '@jest/pattern': 30.4.0 + '@jest/schemas': 30.4.1 + '@types/istanbul-lib-coverage': 2.0.6 + '@types/istanbul-reports': 3.0.4 + '@types/node': 24.13.2 + '@types/yargs': 17.0.35 + chalk: 4.1.2 + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/source-map@0.3.11': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@jridgewell/trace-mapping@0.3.9': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@lukeed/csprng@1.1.0': {} + + '@microsoft/tsdoc@0.16.0': {} + + '@napi-rs/wasm-runtime@1.1.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@tybys/wasm-util': 0.10.2 + optional: true + + '@nestjs/cli@11.0.23(@types/node@24.13.2)(prettier@3.8.4)': + dependencies: + '@angular-devkit/core': 19.2.27(chokidar@4.0.3) + '@angular-devkit/schematics': 19.2.27(chokidar@4.0.3) + '@angular-devkit/schematics-cli': 19.2.27(@types/node@24.13.2)(chokidar@4.0.3) + '@inquirer/prompts': 7.10.1(@types/node@24.13.2) + '@nestjs/schematics': 11.1.0(chokidar@4.0.3)(prettier@3.8.4)(typescript@5.9.3) + ansis: 4.2.0 + chokidar: 4.0.3 + cli-table3: 0.6.5 + commander: 4.1.1 + fork-ts-checker-webpack-plugin: 9.1.0(typescript@5.9.3)(webpack@5.106.2) + glob: 13.0.6 + node-emoji: 1.11.0 + ora: 5.4.1 + tsconfig-paths: 4.2.0 + tsconfig-paths-webpack-plugin: 4.2.0 + typescript: 5.9.3 + webpack: 5.106.2 + webpack-node-externals: 3.0.0 + transitivePeerDependencies: + - '@minify-html/node' + - '@swc/css' + - '@swc/html' + - '@types/node' + - clean-css + - cssnano + - csso + - esbuild + - html-minifier-terser + - lightningcss + - postcss + - prettier + - uglify-js + - webpack-cli + + '@nestjs/common@11.1.26(reflect-metadata@0.2.2)(rxjs@7.8.2)': + dependencies: + file-type: 21.3.4 + iterare: 1.2.1 + load-esm: 1.0.3 + reflect-metadata: 0.2.2 + rxjs: 7.8.2 + tslib: 2.8.1 + uid: 2.0.2 + transitivePeerDependencies: + - supports-color + + '@nestjs/config@4.0.4(@nestjs/common@11.1.26(reflect-metadata@0.2.2)(rxjs@7.8.2))(rxjs@7.8.2)': + dependencies: + '@nestjs/common': 11.1.26(reflect-metadata@0.2.2)(rxjs@7.8.2) + dotenv: 17.4.1 + dotenv-expand: 12.0.3 + lodash: 4.18.1 + rxjs: 7.8.2 + + '@nestjs/core@11.1.26(@nestjs/common@11.1.26(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.26)(reflect-metadata@0.2.2)(rxjs@7.8.2)': + dependencies: + '@nestjs/common': 11.1.26(reflect-metadata@0.2.2)(rxjs@7.8.2) + fast-safe-stringify: 2.1.1 + iterare: 1.2.1 + path-to-regexp: 8.4.2 + reflect-metadata: 0.2.2 + rxjs: 7.8.2 + tslib: 2.8.1 + uid: 2.0.2 + optionalDependencies: + '@nestjs/platform-express': 11.1.26(@nestjs/common@11.1.26(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.26) + + '@nestjs/mapped-types@2.1.1(@nestjs/common@11.1.26(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)': + dependencies: + '@nestjs/common': 11.1.26(reflect-metadata@0.2.2)(rxjs@7.8.2) + reflect-metadata: 0.2.2 + + '@nestjs/platform-express@11.1.26(@nestjs/common@11.1.26(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.26)': + dependencies: + '@nestjs/common': 11.1.26(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.26(@nestjs/common@11.1.26(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.26)(reflect-metadata@0.2.2)(rxjs@7.8.2) + cors: 2.8.6 + express: 5.2.1 + multer: 2.1.1 + path-to-regexp: 8.4.2 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + '@nestjs/schematics@11.1.0(chokidar@4.0.3)(prettier@3.8.4)(typescript@5.9.3)': + dependencies: + '@angular-devkit/core': 19.2.24(chokidar@4.0.3) + '@angular-devkit/schematics': 19.2.24(chokidar@4.0.3) + comment-json: 5.0.0 + jsonc-parser: 3.3.1 + pluralize: 8.0.0 + typescript: 5.9.3 + optionalDependencies: + prettier: 3.8.4 + transitivePeerDependencies: + - chokidar + + '@nestjs/swagger@11.4.4(@nestjs/common@11.1.26(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.26)(reflect-metadata@0.2.2)': + dependencies: + '@microsoft/tsdoc': 0.16.0 + '@nestjs/common': 11.1.26(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.26(@nestjs/common@11.1.26(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.26)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/mapped-types': 2.1.1(@nestjs/common@11.1.26(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2) + js-yaml: 4.1.1 + lodash: 4.18.1 + path-to-regexp: 8.4.2 + reflect-metadata: 0.2.2 + swagger-ui-dist: 5.32.6 + + '@nestjs/testing@11.1.26(@nestjs/common@11.1.26(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.26)(@nestjs/platform-express@11.1.26)': + dependencies: + '@nestjs/common': 11.1.26(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.26(@nestjs/common@11.1.26(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.26)(reflect-metadata@0.2.2)(rxjs@7.8.2) + tslib: 2.8.1 + optionalDependencies: + '@nestjs/platform-express': 11.1.26(@nestjs/common@11.1.26(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.26) + + '@pkgjs/parseargs@0.11.0': + optional: true + + '@pkgr/core@0.3.6': {} + + '@prisma/client@6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3)': + optionalDependencies: + prisma: 6.19.3(typescript@5.9.3) + typescript: 5.9.3 + + '@prisma/config@6.19.3': + dependencies: + c12: 3.1.0 + deepmerge-ts: 7.1.5 + effect: 3.21.0 + empathic: 2.0.0 + transitivePeerDependencies: + - magicast + + '@prisma/debug@6.19.3': {} + + '@prisma/engines-version@7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7': {} + + '@prisma/engines@6.19.3': + dependencies: + '@prisma/debug': 6.19.3 + '@prisma/engines-version': 7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7 + '@prisma/fetch-engine': 6.19.3 + '@prisma/get-platform': 6.19.3 + + '@prisma/fetch-engine@6.19.3': + dependencies: + '@prisma/debug': 6.19.3 + '@prisma/engines-version': 7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7 + '@prisma/get-platform': 6.19.3 + + '@prisma/get-platform@6.19.3': + dependencies: + '@prisma/debug': 6.19.3 + + '@scarf/scarf@1.4.0': {} + + '@sinclair/typebox@0.34.49': {} + + '@sinonjs/commons@3.0.1': + dependencies: + type-detect: 4.0.8 + + '@sinonjs/fake-timers@15.4.0': + dependencies: + '@sinonjs/commons': 3.0.1 + + '@standard-schema/spec@1.1.0': {} + + '@tokenizer/inflate@0.4.1': + dependencies: + debug: 4.4.3 + token-types: 6.1.2 + transitivePeerDependencies: + - supports-color + + '@tokenizer/token@0.3.0': {} + + '@tsconfig/node10@1.0.12': {} + + '@tsconfig/node12@1.0.11': {} + + '@tsconfig/node14@1.0.3': {} + + '@tsconfig/node16@1.0.4': {} + + '@tybys/wasm-util@0.10.2': + dependencies: + tslib: 2.8.1 + optional: true + + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + '@types/babel__generator': 7.27.0 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.28.0 + + '@types/babel__generator@7.27.0': + dependencies: + '@babel/types': 7.29.7 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + + '@types/babel__traverse@7.28.0': + dependencies: + '@babel/types': 7.29.7 + + '@types/eslint-scope@3.7.7': + dependencies: + '@types/eslint': 9.6.1 + '@types/estree': 1.0.9 + + '@types/eslint@9.6.1': + dependencies: + '@types/estree': 1.0.9 + '@types/json-schema': 7.0.15 + + '@types/estree@1.0.9': {} + + '@types/istanbul-lib-coverage@2.0.6': {} + + '@types/istanbul-lib-report@3.0.3': + dependencies: + '@types/istanbul-lib-coverage': 2.0.6 + + '@types/istanbul-reports@3.0.4': + dependencies: + '@types/istanbul-lib-report': 3.0.3 + + '@types/jest@30.0.0': + dependencies: + expect: 30.4.1 + pretty-format: 30.4.1 + + '@types/json-schema@7.0.15': {} + + '@types/node@24.13.2': + dependencies: + undici-types: 7.18.2 + + '@types/stack-utils@2.0.3': {} + + '@types/yargs-parser@21.0.3': {} + + '@types/yargs@17.0.35': + dependencies: + '@types/yargs-parser': 21.0.3 + + '@typescript-eslint/eslint-plugin@8.61.0(@typescript-eslint/parser@8.61.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3)': + dependencies: + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.61.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.61.0 + '@typescript-eslint/type-utils': 8.61.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/utils': 8.61.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.61.0 + eslint: 9.39.4(jiti@2.7.0) + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.61.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.61.0 + '@typescript-eslint/types': 8.61.0 + '@typescript-eslint/typescript-estree': 8.61.0(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.61.0 + debug: 4.4.3 + eslint: 9.39.4(jiti@2.7.0) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.61.0(typescript@5.9.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.61.0(typescript@5.9.3) + '@typescript-eslint/types': 8.61.0 + debug: 4.4.3 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.61.0': + dependencies: + '@typescript-eslint/types': 8.61.0 + '@typescript-eslint/visitor-keys': 8.61.0 + + '@typescript-eslint/tsconfig-utils@8.61.0(typescript@5.9.3)': + dependencies: + typescript: 5.9.3 + + '@typescript-eslint/type-utils@8.61.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3)': + dependencies: + '@typescript-eslint/types': 8.61.0 + '@typescript-eslint/typescript-estree': 8.61.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.61.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) + debug: 4.4.3 + eslint: 9.39.4(jiti@2.7.0) + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@8.61.0': {} + + '@typescript-eslint/typescript-estree@8.61.0(typescript@5.9.3)': + dependencies: + '@typescript-eslint/project-service': 8.61.0(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.61.0(typescript@5.9.3) + '@typescript-eslint/types': 8.61.0 + '@typescript-eslint/visitor-keys': 8.61.0 + debug: 4.4.3 + minimatch: 10.2.5 + semver: 7.8.4 + tinyglobby: 0.2.17 + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.61.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3)': + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.7.0)) + '@typescript-eslint/scope-manager': 8.61.0 + '@typescript-eslint/types': 8.61.0 + '@typescript-eslint/typescript-estree': 8.61.0(typescript@5.9.3) + eslint: 9.39.4(jiti@2.7.0) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.61.0': + dependencies: + '@typescript-eslint/types': 8.61.0 + eslint-visitor-keys: 5.0.1 + + '@ungap/structured-clone@1.3.1': {} + + '@unrs/resolver-binding-android-arm-eabi@1.12.2': + optional: true + + '@unrs/resolver-binding-android-arm64@1.12.2': + optional: true + + '@unrs/resolver-binding-darwin-arm64@1.12.2': + optional: true + + '@unrs/resolver-binding-darwin-x64@1.12.2': + optional: true + + '@unrs/resolver-binding-freebsd-x64@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-arm-gnueabihf@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-arm-musleabihf@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-arm64-gnu@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-arm64-musl@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-loong64-gnu@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-loong64-musl@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-ppc64-gnu@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-riscv64-gnu@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-riscv64-musl@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-s390x-gnu@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-x64-gnu@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-x64-musl@1.12.2': + optional: true + + '@unrs/resolver-binding-openharmony-arm64@1.12.2': + optional: true + + '@unrs/resolver-binding-wasm32-wasi@1.12.2': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@napi-rs/wasm-runtime': 1.1.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + optional: true + + '@unrs/resolver-binding-win32-arm64-msvc@1.12.2': + optional: true + + '@unrs/resolver-binding-win32-ia32-msvc@1.12.2': + optional: true + + '@unrs/resolver-binding-win32-x64-msvc@1.12.2': + optional: true + + '@webassemblyjs/ast@1.14.1': + dependencies: + '@webassemblyjs/helper-numbers': 1.13.2 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + + '@webassemblyjs/floating-point-hex-parser@1.13.2': {} + + '@webassemblyjs/helper-api-error@1.13.2': {} + + '@webassemblyjs/helper-buffer@1.14.1': {} + + '@webassemblyjs/helper-numbers@1.13.2': + dependencies: + '@webassemblyjs/floating-point-hex-parser': 1.13.2 + '@webassemblyjs/helper-api-error': 1.13.2 + '@xtuc/long': 4.2.2 + + '@webassemblyjs/helper-wasm-bytecode@1.13.2': {} + + '@webassemblyjs/helper-wasm-section@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-buffer': 1.14.1 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/wasm-gen': 1.14.1 + + '@webassemblyjs/ieee754@1.13.2': + dependencies: + '@xtuc/ieee754': 1.2.0 + + '@webassemblyjs/leb128@1.13.2': + dependencies: + '@xtuc/long': 4.2.2 + + '@webassemblyjs/utf8@1.13.2': {} + + '@webassemblyjs/wasm-edit@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-buffer': 1.14.1 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/helper-wasm-section': 1.14.1 + '@webassemblyjs/wasm-gen': 1.14.1 + '@webassemblyjs/wasm-opt': 1.14.1 + '@webassemblyjs/wasm-parser': 1.14.1 + '@webassemblyjs/wast-printer': 1.14.1 + + '@webassemblyjs/wasm-gen@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/ieee754': 1.13.2 + '@webassemblyjs/leb128': 1.13.2 + '@webassemblyjs/utf8': 1.13.2 + + '@webassemblyjs/wasm-opt@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-buffer': 1.14.1 + '@webassemblyjs/wasm-gen': 1.14.1 + '@webassemblyjs/wasm-parser': 1.14.1 + + '@webassemblyjs/wasm-parser@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-api-error': 1.13.2 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/ieee754': 1.13.2 + '@webassemblyjs/leb128': 1.13.2 + '@webassemblyjs/utf8': 1.13.2 + + '@webassemblyjs/wast-printer@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@xtuc/long': 4.2.2 + + '@xtuc/ieee754@1.2.0': {} + + '@xtuc/long@4.2.2': {} + + accepts@2.0.0: + dependencies: + mime-types: 3.0.2 + negotiator: 1.0.0 + + acorn-import-phases@1.0.4(acorn@8.16.0): + dependencies: + acorn: 8.16.0 + + acorn-jsx@5.3.2(acorn@8.16.0): + dependencies: + acorn: 8.16.0 + + acorn-walk@8.3.5: + dependencies: + acorn: 8.16.0 + + acorn@8.16.0: {} + + ajv-formats@2.1.1(ajv@8.20.0): + optionalDependencies: + ajv: 8.20.0 + + ajv-formats@3.0.1(ajv@8.18.0): + optionalDependencies: + ajv: 8.18.0 + + ajv-keywords@3.5.2(ajv@6.15.0): + dependencies: + ajv: 6.15.0 + + ajv-keywords@5.1.0(ajv@8.20.0): + dependencies: + ajv: 8.20.0 + fast-deep-equal: 3.1.3 + + ajv@6.15.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ajv@8.18.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.2 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + ajv@8.20.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.2 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + ansi-colors@4.1.3: {} + + ansi-escapes@4.3.2: + dependencies: + type-fest: 0.21.3 + + ansi-regex@5.0.1: {} + + ansi-regex@6.2.2: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@5.2.0: {} + + ansi-styles@6.2.3: {} + + ansis@4.2.0: {} + + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.2 + + append-field@1.0.0: {} + + arg@4.1.3: {} + + argparse@1.0.10: + dependencies: + sprintf-js: 1.0.3 + + argparse@2.0.1: {} + + array-timsort@1.0.3: {} + + babel-jest@30.4.1(@babel/core@7.29.7): + dependencies: + '@babel/core': 7.29.7 + '@jest/transform': 30.4.1 + '@types/babel__core': 7.20.5 + babel-plugin-istanbul: 7.0.1 + babel-preset-jest: 30.4.0(@babel/core@7.29.7) + chalk: 4.1.2 + graceful-fs: 4.2.11 + slash: 3.0.0 + transitivePeerDependencies: + - supports-color + + babel-plugin-istanbul@7.0.1: + dependencies: + '@babel/helper-plugin-utils': 7.29.7 + '@istanbuljs/load-nyc-config': 1.1.0 + '@istanbuljs/schema': 0.1.6 + istanbul-lib-instrument: 6.0.3 + test-exclude: 6.0.0 + transitivePeerDependencies: + - supports-color + + babel-plugin-jest-hoist@30.4.0: + dependencies: + '@types/babel__core': 7.20.5 + + babel-preset-current-node-syntax@1.2.0(@babel/core@7.29.7): + dependencies: + '@babel/core': 7.29.7 + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.29.7) + '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.29.7) + '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.29.7) + '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.29.7) + '@babel/plugin-syntax-import-attributes': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.29.7) + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.29.7) + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.29.7) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.29.7) + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.29.7) + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.29.7) + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.29.7) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.29.7) + '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.29.7) + '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.29.7) + + babel-preset-jest@30.4.0(@babel/core@7.29.7): + dependencies: + '@babel/core': 7.29.7 + babel-plugin-jest-hoist: 30.4.0 + babel-preset-current-node-syntax: 1.2.0(@babel/core@7.29.7) + + balanced-match@1.0.2: {} + + balanced-match@4.0.4: {} + + base64-js@1.5.1: {} + + baseline-browser-mapping@2.10.35: {} + + bl@4.1.0: + dependencies: + buffer: 5.7.1 + inherits: 2.0.4 + readable-stream: 3.6.2 + + body-parser@2.2.2: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 4.4.3 + http-errors: 2.0.1 + iconv-lite: 0.7.2 + on-finished: 2.4.1 + qs: 6.15.2 + raw-body: 3.0.2 + type-is: 2.1.0 + transitivePeerDependencies: + - supports-color + + brace-expansion@1.1.15: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@2.1.1: + dependencies: + balanced-match: 1.0.2 + + brace-expansion@5.0.6: + dependencies: + balanced-match: 4.0.4 + + browserslist@4.28.2: + dependencies: + baseline-browser-mapping: 2.10.35 + caniuse-lite: 1.0.30001799 + electron-to-chromium: 1.5.371 + node-releases: 2.0.47 + update-browserslist-db: 1.2.3(browserslist@4.28.2) + + bs-logger@0.2.6: + dependencies: + fast-json-stable-stringify: 2.1.0 + + bser@2.1.1: + dependencies: + node-int64: 0.4.0 + + buffer-from@1.1.2: {} + + buffer@5.7.1: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + + busboy@1.6.0: + dependencies: + streamsearch: 1.1.0 + + bytes@3.1.2: {} + + c12@3.1.0: + dependencies: + chokidar: 4.0.3 + confbox: 0.2.4 + defu: 6.1.7 + dotenv: 16.6.1 + exsolve: 1.0.8 + giget: 2.0.0 + jiti: 2.7.0 + ohash: 2.0.11 + pathe: 2.0.3 + perfect-debounce: 1.0.0 + pkg-types: 2.3.1 + rc9: 2.1.2 + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + callsites@3.1.0: {} + + camelcase@5.3.1: {} + + camelcase@6.3.0: {} + + caniuse-lite@1.0.30001799: {} + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + char-regex@1.0.2: {} + + chardet@2.1.1: {} + + chokidar@4.0.3: + dependencies: + readdirp: 4.1.2 + + chrome-trace-event@1.0.4: {} + + ci-info@4.4.0: {} + + citty@0.1.6: + dependencies: + consola: 3.4.2 + + citty@0.2.2: {} + + cjs-module-lexer@2.2.0: {} + + cli-cursor@3.1.0: + dependencies: + restore-cursor: 3.1.0 + + cli-spinners@2.9.2: {} + + cli-table3@0.6.5: + dependencies: + string-width: 4.2.3 + optionalDependencies: + '@colors/colors': 1.5.0 + + cli-width@4.1.0: {} + + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + clone@1.0.4: {} + + co@4.6.0: {} + + collect-v8-coverage@1.0.3: {} + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + commander@2.20.3: {} + + commander@4.1.1: {} + + comment-json@5.0.0: + dependencies: + array-timsort: 1.0.3 + esprima: 4.0.1 + + concat-map@0.0.1: {} + + concat-stream@2.0.0: + dependencies: + buffer-from: 1.1.2 + inherits: 2.0.4 + readable-stream: 3.6.2 + typedarray: 0.0.6 + + confbox@0.2.4: {} + + consola@3.4.2: {} + + content-disposition@1.1.0: {} + + content-type@1.0.5: {} + + content-type@2.0.0: {} + + convert-source-map@2.0.0: {} + + cookie-signature@1.2.2: {} + + cookie@0.7.2: {} + + cors@2.8.6: + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + + cosmiconfig@8.3.6(typescript@5.9.3): + dependencies: + import-fresh: 3.3.1 + js-yaml: 4.2.0 + parse-json: 5.2.0 + path-type: 4.0.0 + optionalDependencies: + typescript: 5.9.3 + + create-require@1.1.1: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + dedent@1.7.2: {} + + deep-is@0.1.4: {} + + deepmerge-ts@7.1.5: {} + + deepmerge@4.3.1: {} + + defaults@1.0.4: + dependencies: + clone: 1.0.4 + + defu@6.1.7: {} + + depd@2.0.0: {} + + destr@2.0.5: {} + + detect-newline@3.1.0: {} + + diff@4.0.4: {} + + dotenv-expand@12.0.3: + dependencies: + dotenv: 16.6.1 + + dotenv@16.6.1: {} + + dotenv@17.4.1: {} + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + eastasianwidth@0.2.0: {} + + ee-first@1.1.1: {} + + effect@3.21.0: + dependencies: + '@standard-schema/spec': 1.1.0 + fast-check: 3.23.2 + + electron-to-chromium@1.5.371: {} + + emittery@0.13.1: {} + + emoji-regex@8.0.0: {} + + emoji-regex@9.2.2: {} + + empathic@2.0.0: {} + + encodeurl@2.0.0: {} + + enhanced-resolve@5.23.0: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.3 + + error-ex@1.3.4: + dependencies: + is-arrayish: 0.2.1 + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-module-lexer@2.1.0: {} + + es-object-atoms@1.1.2: + dependencies: + es-errors: 1.3.0 + + escalade@3.2.0: {} + + escape-html@1.0.3: {} + + escape-string-regexp@2.0.0: {} + + escape-string-regexp@4.0.0: {} + + eslint-config-prettier@10.1.8(eslint@9.39.4(jiti@2.7.0)): + dependencies: + eslint: 9.39.4(jiti@2.7.0) + + eslint-scope@5.1.1: + dependencies: + esrecurse: 4.3.0 + estraverse: 4.3.0 + + eslint-scope@8.4.0: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@4.2.1: {} + + eslint-visitor-keys@5.0.1: {} + + eslint@9.39.4(jiti@2.7.0): + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.7.0)) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.21.2 + '@eslint/config-helpers': 0.4.2 + '@eslint/core': 0.17.0 + '@eslint/eslintrc': 3.3.5 + '@eslint/js': 9.39.4 + '@eslint/plugin-kit': 0.4.1 + '@humanfs/node': 0.16.8 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.9 + ajv: 6.15.0 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.3 + escape-string-regexp: 4.0.0 + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + esquery: 1.7.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + lodash.merge: 4.6.2 + minimatch: 3.1.5 + natural-compare: 1.4.0 + optionator: 0.9.4 + optionalDependencies: + jiti: 2.7.0 + transitivePeerDependencies: + - supports-color + + espree@10.4.0: + dependencies: + acorn: 8.16.0 + acorn-jsx: 5.3.2(acorn@8.16.0) + eslint-visitor-keys: 4.2.1 + + esprima@4.0.1: {} + + esquery@1.7.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@4.3.0: {} + + estraverse@5.3.0: {} + + esutils@2.0.3: {} + + etag@1.8.1: {} + + events@3.3.0: {} + + execa@5.1.1: + dependencies: + cross-spawn: 7.0.6 + get-stream: 6.0.1 + human-signals: 2.1.0 + is-stream: 2.0.1 + merge-stream: 2.0.0 + npm-run-path: 4.0.1 + onetime: 5.1.2 + signal-exit: 3.0.7 + strip-final-newline: 2.0.0 + + exit-x@0.2.2: {} + + expect@30.4.1: + dependencies: + '@jest/expect-utils': 30.4.1 + '@jest/get-type': 30.1.0 + jest-matcher-utils: 30.4.1 + jest-message-util: 30.4.1 + jest-mock: 30.4.1 + jest-util: 30.4.1 + + express@5.2.1: + dependencies: + accepts: 2.0.0 + body-parser: 2.2.2 + content-disposition: 1.1.0 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.2.2 + debug: 4.4.3 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 2.1.1 + fresh: 2.0.0 + http-errors: 2.0.1 + merge-descriptors: 2.0.0 + mime-types: 3.0.2 + on-finished: 2.4.1 + once: 1.4.0 + parseurl: 1.3.3 + proxy-addr: 2.0.7 + qs: 6.15.2 + range-parser: 1.2.1 + router: 2.2.0 + send: 1.2.1 + serve-static: 2.2.1 + statuses: 2.0.2 + type-is: 2.1.0 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + + exsolve@1.0.8: {} + + fast-check@3.23.2: + dependencies: + pure-rand: 6.1.0 + + fast-deep-equal@3.1.3: {} + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fast-safe-stringify@2.1.1: {} + + fast-uri@3.1.2: {} + + fb-watchman@2.0.2: + dependencies: + bser: 2.1.1 + + fdir@6.5.0(picomatch@4.0.4): + optionalDependencies: + picomatch: 4.0.4 + + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + + file-type@21.3.4: + dependencies: + '@tokenizer/inflate': 0.4.1 + strtok3: 10.3.5 + token-types: 6.1.2 + uint8array-extras: 1.5.0 + transitivePeerDependencies: + - supports-color + + finalhandler@2.1.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + find-up@4.1.0: + dependencies: + locate-path: 5.0.0 + path-exists: 4.0.0 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@4.0.1: + dependencies: + flatted: 3.4.2 + keyv: 4.5.4 + + flatted@3.4.2: {} + + foreground-child@3.3.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + + fork-ts-checker-webpack-plugin@9.1.0(typescript@5.9.3)(webpack@5.106.2): + dependencies: + '@babel/code-frame': 7.29.7 + chalk: 4.1.2 + chokidar: 4.0.3 + cosmiconfig: 8.3.6(typescript@5.9.3) + deepmerge: 4.3.1 + fs-extra: 10.1.0 + memfs: 3.5.3 + minimatch: 3.1.5 + node-abort-controller: 3.1.1 + schema-utils: 3.3.0 + semver: 7.8.4 + tapable: 2.3.3 + typescript: 5.9.3 + webpack: 5.106.2 + + forwarded@0.2.0: {} + + fresh@2.0.0: {} + + fs-extra@10.1.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.2.1 + universalify: 2.0.1 + + fs-monkey@1.1.0: {} + + fs.realpath@1.0.0: {} + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + gensync@1.0.0-beta.2: {} + + get-caller-file@2.0.5: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.4 + math-intrinsics: 1.1.0 + + get-package-type@0.1.0: {} + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.2 + + get-stream@6.0.1: {} + + giget@2.0.0: + dependencies: + citty: 0.1.6 + consola: 3.4.2 + defu: 6.1.7 + node-fetch-native: 1.6.7 + nypm: 0.6.7 + pathe: 2.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + glob-to-regexp@0.4.1: {} + + glob@10.5.0: + dependencies: + foreground-child: 3.3.1 + jackspeak: 3.4.3 + minimatch: 9.0.9 + minipass: 7.1.3 + package-json-from-dist: 1.0.1 + path-scurry: 1.11.1 + + glob@13.0.6: + dependencies: + minimatch: 10.2.5 + minipass: 7.1.3 + path-scurry: 2.0.2 + + glob@7.2.3: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.5 + once: 1.4.0 + path-is-absolute: 1.0.1 + + globals@14.0.0: {} + + gopd@1.2.0: {} + + graceful-fs@4.2.11: {} + + handlebars@4.7.9: + dependencies: + minimist: 1.2.8 + neo-async: 2.6.2 + source-map: 0.6.1 + wordwrap: 1.0.0 + optionalDependencies: + uglify-js: 3.19.3 + + has-flag@4.0.0: {} + + has-symbols@1.1.0: {} + + hasown@2.0.4: + dependencies: + function-bind: 1.1.2 + + html-escaper@2.0.2: {} + + http-errors@2.0.1: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.2 + toidentifier: 1.0.1 + + human-signals@2.1.0: {} + + iconv-lite@0.7.2: + dependencies: + safer-buffer: 2.1.2 + + ieee754@1.2.1: {} + + ignore@5.3.2: {} + + ignore@7.0.5: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + import-local@3.2.0: + dependencies: + pkg-dir: 4.2.0 + resolve-cwd: 3.0.0 + + imurmurhash@0.1.4: {} + + inflight@1.0.6: + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + + inherits@2.0.4: {} + + ipaddr.js@1.9.1: {} + + is-arrayish@0.2.1: {} + + is-extglob@2.1.1: {} + + is-fullwidth-code-point@3.0.0: {} + + is-generator-fn@2.1.0: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-interactive@1.0.0: {} + + is-promise@4.0.0: {} + + is-stream@2.0.1: {} + + is-unicode-supported@0.1.0: {} + + isexe@2.0.0: {} + + istanbul-lib-coverage@3.2.2: {} + + istanbul-lib-instrument@6.0.3: + dependencies: + '@babel/core': 7.29.7 + '@babel/parser': 7.29.7 + '@istanbuljs/schema': 0.1.6 + istanbul-lib-coverage: 3.2.2 + semver: 7.8.4 + transitivePeerDependencies: + - supports-color + + istanbul-lib-report@3.0.1: + dependencies: + istanbul-lib-coverage: 3.2.2 + make-dir: 4.0.0 + supports-color: 7.2.0 + + istanbul-lib-source-maps@5.0.6: + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + debug: 4.4.3 + istanbul-lib-coverage: 3.2.2 + transitivePeerDependencies: + - supports-color + + istanbul-reports@3.2.0: + dependencies: + html-escaper: 2.0.2 + istanbul-lib-report: 3.0.1 + + iterare@1.2.1: {} + + jackspeak@3.4.3: + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + + jest-changed-files@30.4.1: + dependencies: + execa: 5.1.1 + jest-util: 30.4.1 + p-limit: 3.1.0 + + jest-circus@30.4.2: + dependencies: + '@jest/environment': 30.4.1 + '@jest/expect': 30.4.1 + '@jest/test-result': 30.4.1 + '@jest/types': 30.4.1 + '@types/node': 24.13.2 + chalk: 4.1.2 + co: 4.6.0 + dedent: 1.7.2 + is-generator-fn: 2.1.0 + jest-each: 30.4.1 + jest-matcher-utils: 30.4.1 + jest-message-util: 30.4.1 + jest-runtime: 30.4.2 + jest-snapshot: 30.4.1 + jest-util: 30.4.1 + p-limit: 3.1.0 + pretty-format: 30.4.1 + pure-rand: 7.0.1 + slash: 3.0.0 + stack-utils: 2.0.6 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + + jest-cli@30.4.2(@types/node@24.13.2)(ts-node@10.9.2(@types/node@24.13.2)(typescript@5.9.3)): + dependencies: + '@jest/core': 30.4.2(ts-node@10.9.2(@types/node@24.13.2)(typescript@5.9.3)) + '@jest/test-result': 30.4.1 + '@jest/types': 30.4.1 + chalk: 4.1.2 + exit-x: 0.2.2 + import-local: 3.2.0 + jest-config: 30.4.2(@types/node@24.13.2)(ts-node@10.9.2(@types/node@24.13.2)(typescript@5.9.3)) + jest-util: 30.4.1 + jest-validate: 30.4.1 + yargs: 17.7.2 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - esbuild-register + - supports-color + - ts-node + + jest-config@30.4.2(@types/node@24.13.2)(ts-node@10.9.2(@types/node@24.13.2)(typescript@5.9.3)): + dependencies: + '@babel/core': 7.29.7 + '@jest/get-type': 30.1.0 + '@jest/pattern': 30.4.0 + '@jest/test-sequencer': 30.4.1 + '@jest/types': 30.4.1 + babel-jest: 30.4.1(@babel/core@7.29.7) + chalk: 4.1.2 + ci-info: 4.4.0 + deepmerge: 4.3.1 + glob: 10.5.0 + graceful-fs: 4.2.11 + jest-circus: 30.4.2 + jest-docblock: 30.4.0 + jest-environment-node: 30.4.1 + jest-regex-util: 30.4.0 + jest-resolve: 30.4.1 + jest-runner: 30.4.2 + jest-util: 30.4.1 + jest-validate: 30.4.1 + parse-json: 5.2.0 + pretty-format: 30.4.1 + slash: 3.0.0 + strip-json-comments: 3.1.1 + optionalDependencies: + '@types/node': 24.13.2 + ts-node: 10.9.2(@types/node@24.13.2)(typescript@5.9.3) + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + + jest-diff@30.4.1: + dependencies: + '@jest/diff-sequences': 30.4.0 + '@jest/get-type': 30.1.0 + chalk: 4.1.2 + pretty-format: 30.4.1 + + jest-docblock@30.4.0: + dependencies: + detect-newline: 3.1.0 + + jest-each@30.4.1: + dependencies: + '@jest/get-type': 30.1.0 + '@jest/types': 30.4.1 + chalk: 4.1.2 + jest-util: 30.4.1 + pretty-format: 30.4.1 + + jest-environment-node@30.4.1: + dependencies: + '@jest/environment': 30.4.1 + '@jest/fake-timers': 30.4.1 + '@jest/types': 30.4.1 + '@types/node': 24.13.2 + jest-mock: 30.4.1 + jest-util: 30.4.1 + jest-validate: 30.4.1 + + jest-haste-map@30.4.1: + dependencies: + '@jest/types': 30.4.1 + '@types/node': 24.13.2 + anymatch: 3.1.3 + fb-watchman: 2.0.2 + graceful-fs: 4.2.11 + jest-regex-util: 30.4.0 + jest-util: 30.4.1 + jest-worker: 30.4.1 + picomatch: 4.0.4 + walker: 1.0.8 + optionalDependencies: + fsevents: 2.3.3 + + jest-leak-detector@30.4.1: + dependencies: + '@jest/get-type': 30.1.0 + pretty-format: 30.4.1 + + jest-matcher-utils@30.4.1: + dependencies: + '@jest/get-type': 30.1.0 + chalk: 4.1.2 + jest-diff: 30.4.1 + pretty-format: 30.4.1 + + jest-message-util@30.4.1: + dependencies: + '@babel/code-frame': 7.29.7 + '@jest/types': 30.4.1 + '@types/stack-utils': 2.0.3 + chalk: 4.1.2 + graceful-fs: 4.2.11 + jest-util: 30.4.1 + picomatch: 4.0.4 + pretty-format: 30.4.1 + slash: 3.0.0 + stack-utils: 2.0.6 + + jest-mock@30.4.1: + dependencies: + '@jest/types': 30.4.1 + '@types/node': 24.13.2 + jest-util: 30.4.1 + + jest-pnp-resolver@1.2.3(jest-resolve@30.4.1): + optionalDependencies: + jest-resolve: 30.4.1 + + jest-regex-util@30.4.0: {} + + jest-resolve-dependencies@30.4.2: + dependencies: + jest-regex-util: 30.4.0 + jest-snapshot: 30.4.1 + transitivePeerDependencies: + - supports-color + + jest-resolve@30.4.1: + dependencies: + chalk: 4.1.2 + graceful-fs: 4.2.11 + jest-haste-map: 30.4.1 + jest-pnp-resolver: 1.2.3(jest-resolve@30.4.1) + jest-util: 30.4.1 + jest-validate: 30.4.1 + slash: 3.0.0 + unrs-resolver: 1.12.2 + + jest-runner@30.4.2: + dependencies: + '@jest/console': 30.4.1 + '@jest/environment': 30.4.1 + '@jest/test-result': 30.4.1 + '@jest/transform': 30.4.1 + '@jest/types': 30.4.1 + '@types/node': 24.13.2 + chalk: 4.1.2 + emittery: 0.13.1 + exit-x: 0.2.2 + graceful-fs: 4.2.11 + jest-docblock: 30.4.0 + jest-environment-node: 30.4.1 + jest-haste-map: 30.4.1 + jest-leak-detector: 30.4.1 + jest-message-util: 30.4.1 + jest-resolve: 30.4.1 + jest-runtime: 30.4.2 + jest-util: 30.4.1 + jest-watcher: 30.4.1 + jest-worker: 30.4.1 + p-limit: 3.1.0 + source-map-support: 0.5.13 + transitivePeerDependencies: + - supports-color + + jest-runtime@30.4.2: + dependencies: + '@jest/environment': 30.4.1 + '@jest/fake-timers': 30.4.1 + '@jest/globals': 30.4.1 + '@jest/source-map': 30.0.1 + '@jest/test-result': 30.4.1 + '@jest/transform': 30.4.1 + '@jest/types': 30.4.1 + '@types/node': 24.13.2 + chalk: 4.1.2 + cjs-module-lexer: 2.2.0 + collect-v8-coverage: 1.0.3 + glob: 10.5.0 + graceful-fs: 4.2.11 + jest-haste-map: 30.4.1 + jest-message-util: 30.4.1 + jest-mock: 30.4.1 + jest-regex-util: 30.4.0 + jest-resolve: 30.4.1 + jest-snapshot: 30.4.1 + jest-util: 30.4.1 + slash: 3.0.0 + strip-bom: 4.0.0 + transitivePeerDependencies: + - supports-color + + jest-snapshot@30.4.1: + dependencies: + '@babel/core': 7.29.7 + '@babel/generator': 7.29.7 + '@babel/plugin-syntax-jsx': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-syntax-typescript': 7.29.7(@babel/core@7.29.7) + '@babel/types': 7.29.7 + '@jest/expect-utils': 30.4.1 + '@jest/get-type': 30.1.0 + '@jest/snapshot-utils': 30.4.1 + '@jest/transform': 30.4.1 + '@jest/types': 30.4.1 + babel-preset-current-node-syntax: 1.2.0(@babel/core@7.29.7) + chalk: 4.1.2 + expect: 30.4.1 + graceful-fs: 4.2.11 + jest-diff: 30.4.1 + jest-matcher-utils: 30.4.1 + jest-message-util: 30.4.1 + jest-util: 30.4.1 + pretty-format: 30.4.1 + semver: 7.8.4 + synckit: 0.11.13 + transitivePeerDependencies: + - supports-color + + jest-util@30.4.1: + dependencies: + '@jest/types': 30.4.1 + '@types/node': 24.13.2 + chalk: 4.1.2 + ci-info: 4.4.0 + graceful-fs: 4.2.11 + picomatch: 4.0.4 + + jest-validate@30.4.1: + dependencies: + '@jest/get-type': 30.1.0 + '@jest/types': 30.4.1 + camelcase: 6.3.0 + chalk: 4.1.2 + leven: 3.1.0 + pretty-format: 30.4.1 + + jest-watcher@30.4.1: + dependencies: + '@jest/test-result': 30.4.1 + '@jest/types': 30.4.1 + '@types/node': 24.13.2 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + emittery: 0.13.1 + jest-util: 30.4.1 + string-length: 4.0.2 + + jest-worker@27.5.1: + dependencies: + '@types/node': 24.13.2 + merge-stream: 2.0.0 + supports-color: 8.1.1 + + jest-worker@30.4.1: + dependencies: + '@types/node': 24.13.2 + '@ungap/structured-clone': 1.3.1 + jest-util: 30.4.1 + merge-stream: 2.0.0 + supports-color: 8.1.1 + + jest@30.4.2(@types/node@24.13.2)(ts-node@10.9.2(@types/node@24.13.2)(typescript@5.9.3)): + dependencies: + '@jest/core': 30.4.2(ts-node@10.9.2(@types/node@24.13.2)(typescript@5.9.3)) + '@jest/types': 30.4.1 + import-local: 3.2.0 + jest-cli: 30.4.2(@types/node@24.13.2)(ts-node@10.9.2(@types/node@24.13.2)(typescript@5.9.3)) + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - esbuild-register + - supports-color + - ts-node + + jiti@2.7.0: {} + + js-tokens@4.0.0: {} + + js-yaml@3.14.2: + dependencies: + argparse: 1.0.10 + esprima: 4.0.1 + + js-yaml@4.1.1: + dependencies: + argparse: 2.0.1 + + js-yaml@4.2.0: + dependencies: + argparse: 2.0.1 + + jsesc@3.1.0: {} + + json-buffer@3.0.1: {} + + json-parse-even-better-errors@2.3.1: {} + + json-schema-traverse@0.4.1: {} + + json-schema-traverse@1.0.0: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + json5@2.2.3: {} + + jsonc-parser@3.3.1: {} + + jsonfile@6.2.1: + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + leven@3.1.0: {} + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + lines-and-columns@1.2.4: {} + + load-esm@1.0.3: {} + + loader-runner@4.3.2: {} + + locate-path@5.0.0: + dependencies: + p-locate: 4.1.0 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash.memoize@4.1.2: {} + + lodash.merge@4.6.2: {} + + lodash@4.18.1: {} + + log-symbols@4.1.0: + dependencies: + chalk: 4.1.2 + is-unicode-supported: 0.1.0 + + lru-cache@10.4.3: {} + + lru-cache@11.5.1: {} + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + magic-string@0.30.17: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + make-dir@4.0.0: + dependencies: + semver: 7.8.4 + + make-error@1.3.6: {} + + makeerror@1.0.12: + dependencies: + tmpl: 1.0.5 + + math-intrinsics@1.1.0: {} + + media-typer@0.3.0: {} + + media-typer@1.1.0: {} + + memfs@3.5.3: + dependencies: + fs-monkey: 1.1.0 + + merge-descriptors@2.0.0: {} + + merge-stream@2.0.0: {} + + mime-db@1.52.0: {} + + mime-db@1.54.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + mime-types@3.0.2: + dependencies: + mime-db: 1.54.0 + + mimic-fn@2.1.0: {} + + minimatch@10.2.5: + dependencies: + brace-expansion: 5.0.6 + + minimatch@3.1.5: + dependencies: + brace-expansion: 1.1.15 + + minimatch@9.0.9: + dependencies: + brace-expansion: 2.1.1 + + minimist@1.2.8: {} + + minipass@7.1.3: {} + + ms@2.1.3: {} + + multer@2.1.1: + dependencies: + append-field: 1.0.0 + busboy: 1.6.0 + concat-stream: 2.0.0 + type-is: 1.6.18 + + mute-stream@2.0.0: {} + + napi-postinstall@0.3.4: {} + + natural-compare@1.4.0: {} + + negotiator@1.0.0: {} + + neo-async@2.6.2: {} + + node-abort-controller@3.1.1: {} + + node-emoji@1.11.0: + dependencies: + lodash: 4.18.1 + + node-fetch-native@1.6.7: {} + + node-int64@0.4.0: {} + + node-releases@2.0.47: {} + + normalize-path@3.0.0: {} + + npm-run-path@4.0.1: + dependencies: + path-key: 3.1.1 + + nypm@0.6.7: + dependencies: + citty: 0.2.2 + pathe: 2.0.3 + tinyexec: 1.2.4 + + object-assign@4.1.1: {} + + object-inspect@1.13.4: {} + + ohash@2.0.11: {} + + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + onetime@5.1.2: + dependencies: + mimic-fn: 2.1.0 + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + ora@5.4.1: + dependencies: + bl: 4.1.0 + chalk: 4.1.2 + cli-cursor: 3.1.0 + cli-spinners: 2.9.2 + is-interactive: 1.0.0 + is-unicode-supported: 0.1.0 + log-symbols: 4.1.0 + strip-ansi: 6.0.1 + wcwidth: 1.0.1 + + p-limit@2.3.0: + dependencies: + p-try: 2.2.0 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@4.1.0: + dependencies: + p-limit: 2.3.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + p-try@2.2.0: {} + + package-json-from-dist@1.0.1: {} + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + parse-json@5.2.0: + dependencies: + '@babel/code-frame': 7.29.7 + error-ex: 1.3.4 + json-parse-even-better-errors: 2.3.1 + lines-and-columns: 1.2.4 + + parseurl@1.3.3: {} + + path-exists@4.0.0: {} + + path-is-absolute@1.0.1: {} + + path-key@3.1.1: {} + + path-scurry@1.11.1: + dependencies: + lru-cache: 10.4.3 + minipass: 7.1.3 + + path-scurry@2.0.2: + dependencies: + lru-cache: 11.5.1 + minipass: 7.1.3 + + path-to-regexp@8.4.2: {} + + path-type@4.0.0: {} + + pathe@2.0.3: {} + + perfect-debounce@1.0.0: {} + + picocolors@1.1.1: {} + + picomatch@2.3.2: {} + + picomatch@4.0.4: {} + + pirates@4.0.7: {} + + pkg-dir@4.2.0: + dependencies: + find-up: 4.1.0 + + pkg-types@2.3.1: + dependencies: + confbox: 0.2.4 + exsolve: 1.0.8 + pathe: 2.0.3 + + pluralize@8.0.0: {} + + prelude-ls@1.2.1: {} + + prettier@3.8.4: {} + + pretty-format@30.4.1: + dependencies: + '@jest/schemas': 30.4.1 + ansi-styles: 5.2.0 + react-is-18: react-is@18.3.1 + react-is-19: react-is@19.2.7 + + prisma@6.19.3(typescript@5.9.3): + dependencies: + '@prisma/config': 6.19.3 + '@prisma/engines': 6.19.3 + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - magicast + + proxy-addr@2.0.7: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + + punycode@2.3.1: {} + + pure-rand@6.1.0: {} + + pure-rand@7.0.1: {} + + qs@6.15.2: + dependencies: + side-channel: 1.1.1 + + range-parser@1.2.1: {} + + raw-body@3.0.2: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.1 + iconv-lite: 0.7.2 + unpipe: 1.0.0 + + rc9@2.1.2: + dependencies: + defu: 6.1.7 + destr: 2.0.5 + + react-is@18.3.1: {} + + react-is@19.2.7: {} + + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + + readdirp@4.1.2: {} + + reflect-metadata@0.2.2: {} + + require-directory@2.1.1: {} + + require-from-string@2.0.2: {} + + resolve-cwd@3.0.0: + dependencies: + resolve-from: 5.0.0 + + resolve-from@4.0.0: {} + + resolve-from@5.0.0: {} + + restore-cursor@3.1.0: + dependencies: + onetime: 5.1.2 + signal-exit: 3.0.7 + + router@2.2.0: + dependencies: + debug: 4.4.3 + depd: 2.0.0 + is-promise: 4.0.0 + parseurl: 1.3.3 + path-to-regexp: 8.4.2 + transitivePeerDependencies: + - supports-color + + rxjs@7.8.1: + dependencies: + tslib: 2.8.1 + + rxjs@7.8.2: + dependencies: + tslib: 2.8.1 + + safe-buffer@5.2.1: {} + + safer-buffer@2.1.2: {} + + schema-utils@3.3.0: + dependencies: + '@types/json-schema': 7.0.15 + ajv: 6.15.0 + ajv-keywords: 3.5.2(ajv@6.15.0) + + schema-utils@4.3.3: + dependencies: + '@types/json-schema': 7.0.15 + ajv: 8.20.0 + ajv-formats: 2.1.1(ajv@8.20.0) + ajv-keywords: 5.1.0(ajv@8.20.0) + + semver@6.3.1: {} + + semver@7.8.4: {} + + send@1.2.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 2.0.0 + http-errors: 2.0.1 + mime-types: 3.0.2 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + serve-static@2.2.1: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 1.2.1 + transitivePeerDependencies: + - supports-color + + setprototypeof@1.2.0: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + side-channel-list@1.0.1: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.1: + 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 + + signal-exit@3.0.7: {} + + signal-exit@4.1.0: {} + + slash@3.0.0: {} + + source-map-support@0.5.13: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + source-map-support@0.5.21: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + source-map@0.6.1: {} + + source-map@0.7.4: {} + + sprintf-js@1.0.3: {} + + stack-utils@2.0.6: + dependencies: + escape-string-regexp: 2.0.0 + + statuses@2.0.2: {} + + streamsearch@1.1.0: {} + + string-length@4.0.2: + dependencies: + char-regex: 1.0.2 + strip-ansi: 6.0.1 + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.2.0 + + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.2.0: + dependencies: + ansi-regex: 6.2.2 + + strip-bom@3.0.0: {} + + strip-bom@4.0.0: {} + + strip-final-newline@2.0.0: {} + + strip-json-comments@3.1.1: {} + + strtok3@10.3.5: + dependencies: + '@tokenizer/token': 0.3.0 + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-color@8.1.1: + dependencies: + has-flag: 4.0.0 + + swagger-ui-dist@5.32.6: + dependencies: + '@scarf/scarf': 1.4.0 + + symbol-observable@4.0.0: {} + + synckit@0.11.13: + dependencies: + '@pkgr/core': 0.3.6 + + tapable@2.3.3: {} + + terser-webpack-plugin@5.6.1(webpack@5.106.2): + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + jest-worker: 27.5.1 + schema-utils: 4.3.3 + terser: 5.48.0 + webpack: 5.106.2 + + terser@5.48.0: + dependencies: + '@jridgewell/source-map': 0.3.11 + acorn: 8.16.0 + commander: 2.20.3 + source-map-support: 0.5.21 + + test-exclude@6.0.0: + dependencies: + '@istanbuljs/schema': 0.1.6 + glob: 7.2.3 + minimatch: 3.1.5 + + tinyexec@1.2.4: {} + + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + + tmpl@1.0.5: {} + + toidentifier@1.0.1: {} + + token-types@6.1.2: + dependencies: + '@borewit/text-codec': 0.2.2 + '@tokenizer/token': 0.3.0 + ieee754: 1.2.1 + + ts-api-utils@2.5.0(typescript@5.9.3): + dependencies: + typescript: 5.9.3 + + ts-jest@29.4.11(@babel/core@7.29.7)(@jest/transform@30.4.1)(@jest/types@30.4.1)(babel-jest@30.4.1(@babel/core@7.29.7))(jest-util@30.4.1)(jest@30.4.2(@types/node@24.13.2)(ts-node@10.9.2(@types/node@24.13.2)(typescript@5.9.3)))(typescript@5.9.3): + dependencies: + bs-logger: 0.2.6 + fast-json-stable-stringify: 2.1.0 + handlebars: 4.7.9 + jest: 30.4.2(@types/node@24.13.2)(ts-node@10.9.2(@types/node@24.13.2)(typescript@5.9.3)) + json5: 2.2.3 + lodash.memoize: 4.1.2 + make-error: 1.3.6 + semver: 7.8.4 + type-fest: 4.41.0 + typescript: 5.9.3 + yargs-parser: 21.1.1 + optionalDependencies: + '@babel/core': 7.29.7 + '@jest/transform': 30.4.1 + '@jest/types': 30.4.1 + babel-jest: 30.4.1(@babel/core@7.29.7) + jest-util: 30.4.1 + + ts-node@10.9.2(@types/node@24.13.2)(typescript@5.9.3): + dependencies: + '@cspotcode/source-map-support': 0.8.1 + '@tsconfig/node10': 1.0.12 + '@tsconfig/node12': 1.0.11 + '@tsconfig/node14': 1.0.3 + '@tsconfig/node16': 1.0.4 + '@types/node': 24.13.2 + acorn: 8.16.0 + acorn-walk: 8.3.5 + arg: 4.1.3 + create-require: 1.1.1 + diff: 4.0.4 + make-error: 1.3.6 + typescript: 5.9.3 + v8-compile-cache-lib: 3.0.1 + yn: 3.1.1 + + tsconfig-paths-webpack-plugin@4.2.0: + dependencies: + chalk: 4.1.2 + enhanced-resolve: 5.23.0 + tapable: 2.3.3 + tsconfig-paths: 4.2.0 + + tsconfig-paths@4.2.0: + dependencies: + json5: 2.2.3 + minimist: 1.2.8 + strip-bom: 3.0.0 + + tslib@2.8.1: {} + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + type-detect@4.0.8: {} + + type-fest@0.21.3: {} + + type-fest@4.41.0: {} + + type-is@1.6.18: + dependencies: + media-typer: 0.3.0 + mime-types: 2.1.35 + + type-is@2.1.0: + dependencies: + content-type: 2.0.0 + media-typer: 1.1.0 + mime-types: 3.0.2 + + typedarray@0.0.6: {} + + typescript-eslint@8.61.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3): + dependencies: + '@typescript-eslint/eslint-plugin': 8.61.0(@typescript-eslint/parser@8.61.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/parser': 8.61.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.61.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.61.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) + eslint: 9.39.4(jiti@2.7.0) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + typescript@5.9.3: {} + + uglify-js@3.19.3: + optional: true + + uid@2.0.2: + dependencies: + '@lukeed/csprng': 1.1.0 + + uint8array-extras@1.5.0: {} + + undici-types@7.18.2: {} + + universalify@2.0.1: {} + + unpipe@1.0.0: {} + + unrs-resolver@1.12.2: + dependencies: + napi-postinstall: 0.3.4 + optionalDependencies: + '@unrs/resolver-binding-android-arm-eabi': 1.12.2 + '@unrs/resolver-binding-android-arm64': 1.12.2 + '@unrs/resolver-binding-darwin-arm64': 1.12.2 + '@unrs/resolver-binding-darwin-x64': 1.12.2 + '@unrs/resolver-binding-freebsd-x64': 1.12.2 + '@unrs/resolver-binding-linux-arm-gnueabihf': 1.12.2 + '@unrs/resolver-binding-linux-arm-musleabihf': 1.12.2 + '@unrs/resolver-binding-linux-arm64-gnu': 1.12.2 + '@unrs/resolver-binding-linux-arm64-musl': 1.12.2 + '@unrs/resolver-binding-linux-loong64-gnu': 1.12.2 + '@unrs/resolver-binding-linux-loong64-musl': 1.12.2 + '@unrs/resolver-binding-linux-ppc64-gnu': 1.12.2 + '@unrs/resolver-binding-linux-riscv64-gnu': 1.12.2 + '@unrs/resolver-binding-linux-riscv64-musl': 1.12.2 + '@unrs/resolver-binding-linux-s390x-gnu': 1.12.2 + '@unrs/resolver-binding-linux-x64-gnu': 1.12.2 + '@unrs/resolver-binding-linux-x64-musl': 1.12.2 + '@unrs/resolver-binding-openharmony-arm64': 1.12.2 + '@unrs/resolver-binding-wasm32-wasi': 1.12.2 + '@unrs/resolver-binding-win32-arm64-msvc': 1.12.2 + '@unrs/resolver-binding-win32-ia32-msvc': 1.12.2 + '@unrs/resolver-binding-win32-x64-msvc': 1.12.2 + + update-browserslist-db@1.2.3(browserslist@4.28.2): + dependencies: + browserslist: 4.28.2 + escalade: 3.2.0 + picocolors: 1.1.1 + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + util-deprecate@1.0.2: {} + + v8-compile-cache-lib@3.0.1: {} + + v8-to-istanbul@9.3.0: + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + '@types/istanbul-lib-coverage': 2.0.6 + convert-source-map: 2.0.0 + + vary@1.1.2: {} + + walker@1.0.8: + dependencies: + makeerror: 1.0.12 + + watchpack@2.5.1: + dependencies: + glob-to-regexp: 0.4.1 + graceful-fs: 4.2.11 + + wcwidth@1.0.1: + dependencies: + defaults: 1.0.4 + + webpack-node-externals@3.0.0: {} + + webpack-sources@3.5.0: {} + + webpack@5.106.2: + dependencies: + '@types/eslint-scope': 3.7.7 + '@types/estree': 1.0.9 + '@types/json-schema': 7.0.15 + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/wasm-edit': 1.14.1 + '@webassemblyjs/wasm-parser': 1.14.1 + acorn: 8.16.0 + acorn-import-phases: 1.0.4(acorn@8.16.0) + browserslist: 4.28.2 + chrome-trace-event: 1.0.4 + enhanced-resolve: 5.23.0 + es-module-lexer: 2.1.0 + eslint-scope: 5.1.1 + events: 3.3.0 + glob-to-regexp: 0.4.1 + graceful-fs: 4.2.11 + loader-runner: 4.3.2 + mime-db: 1.54.0 + neo-async: 2.6.2 + schema-utils: 4.3.3 + tapable: 2.3.3 + terser-webpack-plugin: 5.6.1(webpack@5.106.2) + watchpack: 2.5.1 + webpack-sources: 3.5.0 + transitivePeerDependencies: + - '@minify-html/node' + - '@swc/core' + - '@swc/css' + - '@swc/html' + - clean-css + - cssnano + - csso + - esbuild + - html-minifier-terser + - lightningcss + - postcss + - uglify-js + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + word-wrap@1.2.5: {} + + wordwrap@1.0.0: {} + + wrap-ansi@6.2.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@8.1.0: + dependencies: + ansi-styles: 6.2.3 + string-width: 5.1.2 + strip-ansi: 7.2.0 + + wrappy@1.0.2: {} + + write-file-atomic@5.0.1: + dependencies: + imurmurhash: 0.1.4 + signal-exit: 4.1.0 + + y18n@5.0.8: {} + + yallist@3.1.1: {} + + yargs-parser@21.1.1: {} + + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + + yn@3.1.1: {} + + yocto-queue@0.1.0: {} + + yoctocolors-cjs@2.1.3: {} + + zod@4.4.3: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..ccbac80 --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +packages: + - "." diff --git a/prisma/migrations/README.md b/prisma/migrations/README.md new file mode 100644 index 0000000..41e9a74 --- /dev/null +++ b/prisma/migrations/README.md @@ -0,0 +1,12 @@ +# Prisma Migration Structure + +This project uses Prisma as the default data access layer. + +Migration policy: + +- Create schema changes with `pnpm prisma:migrate:dev --name ` after the final MySQL connection details are confirmed. +- Commit generated folders under `prisma/migrations/_/migration.sql`. +- Run migration checks in deployment before starting the API. +- Keep raw SQL exceptional. If native SQL is required for performance or vendor-specific behavior, place it behind a Repository method and document why Prisma Client is not sufficient. + +The first-stage schema is defined in `prisma/schema.prisma`; no live migration is executed until a local or staging MySQL database is explicitly provided. diff --git a/prisma/schema.prisma b/prisma/schema.prisma new file mode 100644 index 0000000..12f102d --- /dev/null +++ b/prisma/schema.prisma @@ -0,0 +1,255 @@ +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "mysql" + url = env("DATABASE_URL") +} + +enum ProjectStatus { + ACTIVE + ARCHIVED +} + +enum EnvironmentKind { + TEST + PRODUCTION +} + +enum DeployRunStatus { + PENDING + QUEUED + RUNNING + SUCCESS + FAILED + CANCELED +} + +enum DeployRunTrigger { + MANUAL + GITEA_WEBHOOK + RETRY +} + +enum RunStepStatus { + PENDING + RUNNING + SUCCESS + FAILED + SKIPPED + CANCELED +} + +enum SecretProvider { + JENKINS + GITEA + WECOM + LLM + INTERNAL +} + +enum AgentInvocationPurpose { + RISK_SUMMARY + FAILURE_DIAGNOSIS + RUNBOOK_QA + RELEASE_NOTES + INCIDENT_REVIEW +} + +enum OutboxStatus { + PENDING + SENT + FAILED + DEAD +} + +model Project { + id String @id @default(cuid()) + key String @unique + name String + repositoryUrl String? @db.VarChar(512) + defaultBranch String @default("develop") + status ProjectStatus @default(ACTIVE) + metadata Json? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + environments Environment[] + deployJobs DeployJob[] + deployRuns DeployRun[] + + @@map("projects") +} + +model Environment { + id String @id @default(cuid()) + projectId String + name EnvironmentKind + displayName String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + project Project @relation(fields: [projectId], references: [id], onDelete: Cascade) + deployJobs DeployJob[] + deployRuns DeployRun[] + + @@unique([projectId, name]) + @@index([projectId]) + @@map("environments") +} + +model DeployJob { + id String @id @default(cuid()) + projectId String + environmentId String + jenkinsJobPath String + jenkinsParameters Json? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + project Project @relation(fields: [projectId], references: [id], onDelete: Cascade) + environment Environment @relation(fields: [environmentId], references: [id], onDelete: Cascade) + deployRuns DeployRun[] + + @@unique([projectId, environmentId]) + @@index([environmentId]) + @@map("deploy_jobs") +} + +model DeployRun { + id String @id @default(cuid()) + projectId String + environmentId String + jobId String? + status DeployRunStatus @default(PENDING) + trigger DeployRunTrigger @default(MANUAL) + ref String + operator String + idempotencyKey String @unique + jenkinsQueueId String? + jenkinsBuildNumber Int? + failureSummary String? @db.Text + metadata Json? + version Int @default(1) + startedAt DateTime? + finishedAt DateTime? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + project Project @relation(fields: [projectId], references: [id], onDelete: Restrict) + environment Environment @relation(fields: [environmentId], references: [id], onDelete: Restrict) + job DeployJob? @relation(fields: [jobId], references: [id], onDelete: SetNull) + steps RunStep[] + auditLogs AuditLog[] + agentInvocations AgentInvocation[] + outboxMessages OutboxMessage[] + + @@index([projectId, environmentId, createdAt]) + @@index([status, createdAt]) + @@map("deploy_runs") +} + +model RunStep { + id String @id @default(cuid()) + deployRunId String + bpmnNodeId String + name String + status RunStepStatus @default(PENDING) + order Int @default(0) + errorSummary String? @db.Text + metadata Json? + startedAt DateTime? + finishedAt DateTime? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + deployRun DeployRun @relation(fields: [deployRunId], references: [id], onDelete: Cascade) + + @@unique([deployRunId, bpmnNodeId]) + @@index([deployRunId, order]) + @@map("run_steps") +} + +model AuditLog { + id String @id @default(cuid()) + deployRunId String? + actorId String? + actorName String? + action String + resourceType String + resourceId String? + requestId String? + sourceIp String? + userAgent String? + before Json? + after Json? + parameterDigest Json? + createdAt DateTime @default(now()) + + deployRun DeployRun? @relation(fields: [deployRunId], references: [id], onDelete: SetNull) + + @@index([resourceType, resourceId, createdAt]) + @@index([actorId, createdAt]) + @@map("audit_logs") +} + +model Secret { + id String @id @default(cuid()) + provider SecretProvider + scopeType String + scopeId String + name String + encryptedValue String @db.Text + version Int @default(1) + rotatedAt DateTime? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@unique([provider, scopeType, scopeId, name]) + @@index([scopeType, scopeId]) + @@map("secrets") +} + +model AgentInvocation { + id String @id @default(cuid()) + deployRunId String? + purpose AgentInvocationPurpose + actorId String? + actorName String? + model String? + promptVersion String + inputDigest Json + outputDigest Json? + tokenEstimate Int? + status String + errorSummary String? @db.Text + createdAt DateTime @default(now()) + completedAt DateTime? + + deployRun DeployRun? @relation(fields: [deployRunId], references: [id], onDelete: SetNull) + + @@index([purpose, createdAt]) + @@index([deployRunId, createdAt]) + @@map("agent_invocations") +} + +model OutboxMessage { + id String @id @default(cuid()) + deployRunId String? + channel String + template String + payload Json + status OutboxStatus @default(PENDING) + attemptCount Int @default(0) + nextAttemptAt DateTime? + lastError String? @db.Text + idempotencyKey String @unique + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + deployRun DeployRun? @relation(fields: [deployRunId], references: [id], onDelete: SetNull) + + @@index([status, nextAttemptAt]) + @@map("outbox_messages") +} diff --git a/src/agent/agent.controller.ts b/src/agent/agent.controller.ts new file mode 100644 index 0000000..c7eead7 --- /dev/null +++ b/src/agent/agent.controller.ts @@ -0,0 +1,37 @@ +import { Body, Controller, Get, Post } from '@nestjs/common'; +import { + ApiBody, + ApiCreatedResponse, + ApiOkResponse, + ApiTags, +} from '@nestjs/swagger'; +import { ZodValidationPipe } from '../common/pipes/zod-validation.pipe'; +import { AgentInvocation } from './agent.types'; +import { AgentService } from './agent.service'; +import { + CreateAgentInvocationDto, + CreateAgentInvocationInput, + createAgentInvocationSchema, +} from './create-agent-invocation.dto'; + +@ApiTags('agent') +@Controller('agent/invocations') +export class AgentController { + constructor(private readonly agentService: AgentService) {} + + @Get() + @ApiOkResponse({ description: 'Agent invocation audit list.' }) + listInvocations(): AgentInvocation[] { + return this.agentService.listInvocations(); + } + + @Post() + @ApiBody({ type: CreateAgentInvocationDto }) + @ApiCreatedResponse({ description: 'Create a DevOps-scoped agent invocation.' }) + async createInvocation( + @Body(new ZodValidationPipe(createAgentInvocationSchema)) + body: CreateAgentInvocationInput, + ): Promise { + return this.agentService.createInvocation(body); + } +} diff --git a/src/agent/agent.module.ts b/src/agent/agent.module.ts new file mode 100644 index 0000000..d837130 --- /dev/null +++ b/src/agent/agent.module.ts @@ -0,0 +1,13 @@ +import { Module } from '@nestjs/common'; +import { AuditModule } from '../audit/audit.module'; +import { AgentController } from './agent.controller'; +import { AgentService } from './agent.service'; +import { LlmClient } from './llm.client'; + +@Module({ + imports: [AuditModule], + controllers: [AgentController], + providers: [AgentService, LlmClient], + exports: [AgentService, LlmClient], +}) +export class AgentModule {} diff --git a/src/agent/agent.service.spec.ts b/src/agent/agent.service.spec.ts new file mode 100644 index 0000000..2f1cd70 --- /dev/null +++ b/src/agent/agent.service.spec.ts @@ -0,0 +1,103 @@ +import { AuditService } from '../audit/audit.service'; +import { AppError } from '../common/errors/app-error'; +import { AgentService } from './agent.service'; +import { CreateAgentInvocationInput } from './create-agent-invocation.dto'; +import { LlmClient } from './llm.client'; + +/** + * 锁定 Agent 入口只服务 DevOps 场景,避免后续误扩成泛聊天接口。 + */ +describe('AgentService', () => { + function createService(options?: { + llmConfigured?: boolean; + llmText?: string; + }) { + const audit = { + record: jest.fn(), + } satisfies Pick; + const llm = { + healthSummary: jest.fn(() => + options?.llmConfigured + ? { status: 'ok' as const } + : { status: 'not_configured' as const, message: 'Missing LLM_API_KEY' }, + ), + complete: jest.fn(() => + Promise.resolve({ + text: options?.llmText ?? 'LLM generated DevOps answer', + model: 'test-model', + }), + ), + } satisfies Pick; + + return { + audit, + llm, + service: new AgentService( + audit as unknown as AuditService, + llm as unknown as LlmClient, + ), + }; + } + + it('creates a DevOps-scoped mock invocation when LLM is not configured', async () => { + const { audit, llm, service } = createService(); + + const invocation = await service.createInvocation({ + type: 'failure-diagnosis', + runId: 'run_001', + projectKey: 'role-user', + promptSummary: 'Analyze latest failed Jenkins log excerpt.', + }); + + expect(invocation.status).toBe('success'); + expect(invocation.resultMarkdown).toContain('失败诊断摘要'); + expect(llm.complete).not.toHaveBeenCalled(); + expect(audit.record).toHaveBeenCalledWith( + expect.objectContaining({ + action: 'AGENT_INVOCATION_CREATED', + resourceType: 'agent_invocation', + }), + ); + }); + + it('creates a DevOps-scoped LLM invocation when configured', async () => { + const { audit, llm, service } = createService({ + llmConfigured: true, + llmText: '真实 LLM 运维分析结果', + }); + + const invocation = await service.createInvocation({ + type: 'release-risk', + projectKey: 'access-manage', + promptSummary: 'Analyze production tag risk.', + }); + + expect(invocation.status).toBe('success'); + expect(invocation.resultMarkdown).toBe('真实 LLM 运维分析结果'); + expect(llm.complete).toHaveBeenCalledWith( + expect.objectContaining({ + purpose: 'release-risk', + promptVersion: 'devops-agent-v1', + }), + ); + expect(audit.record).toHaveBeenCalledWith( + expect.objectContaining({ + after: expect.objectContaining({ + mode: 'llm', + model: 'test-model', + }), + }), + ); + }); + + it('rejects non-DevOps invocation types', async () => { + const { service } = createService(); + + await expect( + service.createInvocation({ + type: 'general-chat', + promptSummary: 'Tell me anything.', + } as unknown as CreateAgentInvocationInput), + ).rejects.toBeInstanceOf(AppError); + }); +}); diff --git a/src/agent/agent.service.ts b/src/agent/agent.service.ts new file mode 100644 index 0000000..d09393c --- /dev/null +++ b/src/agent/agent.service.ts @@ -0,0 +1,147 @@ +import { Injectable } from '@nestjs/common'; +import { AppError } from '../common/errors/app-error'; +import { AuditService } from '../audit/audit.service'; +import { redactSensitive } from '../common/security/redact-sensitive'; +import { AgentInvocation, AgentPurpose } from './agent.types'; +import { CreateAgentInvocationInput } from './create-agent-invocation.dto'; +import { LlmClient } from './llm.client'; + +const allowedTypes: AgentPurpose[] = [ + 'release-risk', + 'failure-diagnosis', + 'runbook-qa', + 'release-note', + 'incident-review', +]; + +const mockResults: Record = { + 'release-risk': + '发布风险摘要:当前为第一阶段 mock 分析。真实接入后会读取 Gitea ref diff、历史失败记录和项目部署策略。', + 'failure-diagnosis': + '失败诊断摘要:当前为第一阶段 mock 分析。真实接入后会基于 Jenkins progressiveText 日志和 run steps 生成建议。', + 'runbook-qa': + 'Runbook 回答:当前仅允许围绕 DevOps 平台文档、项目 README 和白名单运维资料回答。', + 'release-note': + '发布说明草稿:当前为 mock 草稿。真实接入后会根据 commit、PR 和 tag 信息生成。', + 'incident-review': + '事故复盘草稿:当前为 mock 草稿。真实接入后会汇总失败 run、耗时、日志摘要和人工处理记录。', +}; + +@Injectable() +export class AgentService { + private readonly invocations: AgentInvocation[] = []; + + constructor( + private readonly auditService: AuditService, + private readonly llmClient: LlmClient, + ) {} + + listInvocations(): AgentInvocation[] { + return [...this.invocations].sort((left, right) => + right.createdAt.localeCompare(left.createdAt), + ); + } + + async createInvocation(input: CreateAgentInvocationInput): Promise { + if (!allowedTypes.includes(input.type)) { + throw new AppError('VALIDATION_FAILED', 'Unsupported agent invocation type', 400, { + type: input.type, + }); + } + + const llmHealth = this.llmClient.healthSummary(); + const baseInvocation = { + id: `agent_${Date.now()}_${this.invocations.length + 1}`, + runId: input.runId, + projectKey: input.projectKey, + type: input.type, + promptSummary: input.promptSummary, + createdAt: new Date().toISOString(), + }; + let invocation: AgentInvocation; + + if (llmHealth.status === 'ok') { + try { + const completion = await this.llmClient.complete({ + purpose: input.type, + promptVersion: 'devops-agent-v1', + systemPrompt: this.systemPromptFor(input.type), + userPrompt: this.userPromptFor(input), + }); + + invocation = { + ...baseInvocation, + status: 'success', + resultMarkdown: completion.text || mockResults[input.type], + }; + this.recordInvocationAudit(invocation, 'llm', completion.model); + } catch (error) { + invocation = { + ...baseInvocation, + status: 'failed', + resultMarkdown: + error instanceof Error + ? redactSensitive({ message: error.message }).message + : 'LLM invocation failed', + }; + this.recordInvocationAudit(invocation, 'llm_failed'); + } + } else { + invocation = { + ...baseInvocation, + status: 'success', + resultMarkdown: mockResults[input.type], + }; + this.recordInvocationAudit(invocation, 'mock', undefined, llmHealth.message); + } + + this.invocations.unshift(invocation); + return invocation; + } + + private recordInvocationAudit( + invocation: AgentInvocation, + mode: 'mock' | 'llm' | 'llm_failed', + model?: string, + reason?: string, + ): void { + const action = + invocation.status === 'failed' + ? 'AGENT_INVOCATION_FAILED' + : 'AGENT_INVOCATION_CREATED'; + + this.auditService.record({ + action, + resourceType: 'agent_invocation', + resourceId: invocation.id, + after: { + type: invocation.type, + runId: invocation.runId, + projectKey: invocation.projectKey, + mode, + model, + reason, + }, + parameterDigest: { + promptSummary: invocation.promptSummary, + }, + }); + } + + private systemPromptFor(type: AgentPurpose): string { + return [ + '你是 DevOps 运维平台内的受限 Agent,只能围绕发布、构建、Jenkins、Gitea、通知平台、BPMN 流程、回滚和运行记录回答。', + '不要输出或猜测任何密钥、token、webhook、cookie、私钥、环境变量原文。', + '只给出与当前项目运维目标有关的结论、风险、排查步骤或草稿。', + `当前任务类型:${type}`, + ].join('\n'); + } + + private userPromptFor(input: CreateAgentInvocationInput): string { + return [ + `项目:${input.projectKey ?? '未指定'}`, + `Run:${input.runId ?? '未指定'}`, + `用户输入:${input.promptSummary}`, + ].join('\n'); + } +} diff --git a/src/agent/agent.types.ts b/src/agent/agent.types.ts new file mode 100644 index 0000000..77247b1 --- /dev/null +++ b/src/agent/agent.types.ts @@ -0,0 +1,32 @@ +export type AgentPurpose = + | 'release-risk' + | 'failure-diagnosis' + | 'runbook-qa' + | 'release-note' + | 'incident-review'; + +export type LlmCompletionInput = { + purpose: AgentPurpose; + promptVersion: string; + systemPrompt: string; + userPrompt: string; + operator?: string; +}; + +export type LlmCompletionResult = { + text: string; + model: string; +}; + +export type AgentInvocationStatus = 'pending' | 'running' | 'success' | 'failed'; + +export type AgentInvocation = { + id: string; + runId?: string; + projectKey?: string; + type: AgentPurpose; + status: AgentInvocationStatus; + promptSummary: string; + resultMarkdown?: string; + createdAt: string; +}; diff --git a/src/agent/create-agent-invocation.dto.ts b/src/agent/create-agent-invocation.dto.ts new file mode 100644 index 0000000..d84b81a --- /dev/null +++ b/src/agent/create-agent-invocation.dto.ts @@ -0,0 +1,58 @@ +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { z } from 'zod'; +import { AgentPurpose } from './agent.types'; + +const agentPurposeSchema = z.enum([ + 'release-risk', + 'failure-diagnosis', + 'runbook-qa', + 'release-note', + 'incident-review', +]); + +const optionalTrimmedString = (maxLength: number) => + z + .string() + .trim() + .max(maxLength) + .optional() + .or(z.literal('').transform(() => undefined)); + +/** + * Agent 入口只接受 DevOps 场景参数,防止演变成通用聊天代理。 + */ +export const createAgentInvocationSchema = z.object({ + type: agentPurposeSchema, + runId: optionalTrimmedString(120), + projectKey: optionalTrimmedString(80), + promptSummary: z.string().trim().min(1).max(2000), +}); + +export type CreateAgentInvocationInput = z.infer< + typeof createAgentInvocationSchema +>; + +export class CreateAgentInvocationDto { + @ApiProperty({ + enum: [ + 'release-risk', + 'failure-diagnosis', + 'runbook-qa', + 'release-note', + 'incident-review', + ], + example: 'failure-diagnosis', + }) + type!: AgentPurpose; + + @ApiPropertyOptional({ example: 'run-1052' }) + runId?: string; + + @ApiPropertyOptional({ example: 'access-manage' }) + projectKey?: string; + + @ApiProperty({ + example: 'Analyze latest failed Jenkins log excerpt for access-manage test.', + }) + promptSummary!: string; +} diff --git a/src/agent/llm.client.ts b/src/agent/llm.client.ts new file mode 100644 index 0000000..06f5f46 --- /dev/null +++ b/src/agent/llm.client.ts @@ -0,0 +1,75 @@ +import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { IntegrationRequestError, IntegrationUnavailableError } from '../common/errors/integration-error'; +import { redactSensitive } from '../common/security/redact-sensitive'; +import { EnvConfig } from '../config/env.schema'; +import { LlmCompletionInput, LlmCompletionResult } from './agent.types'; + +@Injectable() +export class LlmClient { + constructor(private readonly config: ConfigService) {} + + healthSummary(): { status: 'ok' | 'not_configured'; message?: string } { + const missing = this.missingConfig(); + return missing.length === 0 + ? { status: 'ok' } + : { status: 'not_configured', message: `Missing ${missing.join(', ')}` }; + } + + async complete(input: LlmCompletionInput): Promise { + this.assertConfigured(); + const baseUrl = this.config.get('LLM_BASE_URL', { infer: true }) ?? ''; + const apiKey = this.config.get('LLM_API_KEY', { infer: true }) ?? ''; + const model = this.config.get('LLM_MODEL', { infer: true }) ?? ''; + const response = await fetch(`${baseUrl}/chat/completions`, { + method: 'POST', + headers: { + authorization: `Bearer ${apiKey}`, + 'content-type': 'application/json', + }, + body: JSON.stringify({ + model, + messages: [ + { role: 'system', content: input.systemPrompt }, + { role: 'user', content: input.userPrompt }, + ], + }), + }); + + if (!response.ok) { + throw new IntegrationRequestError('llm', 'LLM request failed', { + status: response.status, + purpose: input.purpose, + request: redactSensitive(input), + }); + } + + const payload = (await response.json()) as { + choices?: Array<{ message?: { content?: string } }>; + model?: string; + }; + + return { + text: payload.choices?.[0]?.message?.content ?? '', + model: payload.model ?? model, + }; + } + + private assertConfigured(): void { + const missing = this.missingConfig(); + + if (missing.length > 0) { + throw new IntegrationUnavailableError('llm', missing); + } + } + + private missingConfig(): string[] { + const entries: Array<[string, string | undefined]> = [ + ['LLM_BASE_URL', this.config.get('LLM_BASE_URL', { infer: true })], + ['LLM_API_KEY', this.config.get('LLM_API_KEY', { infer: true })], + ['LLM_MODEL', this.config.get('LLM_MODEL', { infer: true })], + ]; + + return entries.filter(([, value]) => !value).map(([key]) => key); + } +} diff --git a/src/app.module.ts b/src/app.module.ts new file mode 100644 index 0000000..faebc50 --- /dev/null +++ b/src/app.module.ts @@ -0,0 +1,39 @@ +import { Module } from '@nestjs/common'; +import { ConfigModule } from '@nestjs/config'; +import { AgentModule } from './agent/agent.module'; +import { AuditModule } from './audit/audit.module'; +import { DeployRunsModule } from './deploy-runs/deploy-runs.module'; +import { validateEnv } from './config/env.schema'; +import { GiteaModule } from './integrations/gitea/gitea.module'; +import { GiteaWebhookModule } from './webhooks/gitea/gitea-webhook.module'; +import { HealthModule } from './health/health.module'; +import { JenkinsModule } from './integrations/jenkins/jenkins.module'; +import { ProcessDefinitionsModule } from './process-definitions/process-definitions.module'; +import { PrismaModule } from './prisma/prisma.module'; +import { ProjectsModule } from './projects/projects.module'; +import { SettingsModule } from './settings/settings.module'; +import { WeComModule } from './notifications/wecom/wecom.module'; + +@Module({ + imports: [ + ConfigModule.forRoot({ + cache: true, + expandVariables: true, + isGlobal: true, + validate: validateEnv, + }), + PrismaModule, + AuditModule, + AgentModule, + JenkinsModule, + GiteaModule, + GiteaWebhookModule, + WeComModule, + HealthModule, + SettingsModule, + ProcessDefinitionsModule, + ProjectsModule, + DeployRunsModule, + ], +}) +export class AppModule {} diff --git a/src/audit/audit.controller.ts b/src/audit/audit.controller.ts new file mode 100644 index 0000000..f7e90de --- /dev/null +++ b/src/audit/audit.controller.ts @@ -0,0 +1,15 @@ +import { Controller, Get } from '@nestjs/common'; +import { ApiOkResponse, ApiTags } from '@nestjs/swagger'; +import { AuditRecordSummary, AuditService } from './audit.service'; + +@ApiTags('audit') +@Controller('audit-logs') +export class AuditController { + constructor(private readonly auditService: AuditService) {} + + @Get() + @ApiOkResponse({ description: 'Recent sanitized audit records.' }) + listRecentRecords(): AuditRecordSummary[] { + return this.auditService.getRecentRecords(); + } +} diff --git a/src/audit/audit.module.ts b/src/audit/audit.module.ts new file mode 100644 index 0000000..88eb9fd --- /dev/null +++ b/src/audit/audit.module.ts @@ -0,0 +1,11 @@ +import { Global, Module } from '@nestjs/common'; +import { AuditController } from './audit.controller'; +import { AuditService } from './audit.service'; + +@Global() +@Module({ + controllers: [AuditController], + providers: [AuditService], + exports: [AuditService], +}) +export class AuditModule {} diff --git a/src/audit/audit.service.spec.ts b/src/audit/audit.service.spec.ts new file mode 100644 index 0000000..8d36390 --- /dev/null +++ b/src/audit/audit.service.spec.ts @@ -0,0 +1,27 @@ +import { AuditService } from './audit.service'; + +/** + * 审计记录必须先脱敏再暴露给前端或 Agent。 + */ +describe('AuditService', () => { + it('stores recent audit records with sensitive values redacted', () => { + const service = new AuditService(); + + service.record({ + action: 'SECRET_CHANGED', + resourceType: 'secret', + parameterDigest: { + token: 'abc123456789', + normal: 'visible', + }, + }); + + const [record] = service.getRecentRecords(); + + expect(record.action).toBe('SECRET_CHANGED'); + expect(record.parameterDigest).toEqual({ + token: '[REDACTED]', + normal: 'visible', + }); + }); +}); diff --git a/src/audit/audit.service.ts b/src/audit/audit.service.ts new file mode 100644 index 0000000..c097778 --- /dev/null +++ b/src/audit/audit.service.ts @@ -0,0 +1,36 @@ +import { Injectable } from '@nestjs/common'; +import { redactSensitive } from '../common/security/redact-sensitive'; + +export type AuditRecordInput = { + action: string; + resourceType: string; + resourceId?: string; + actorId?: string; + actorName?: string; + before?: Record; + after?: Record; + parameterDigest?: Record; +}; + +export type AuditRecordSummary = AuditRecordInput & { createdAt: string }; + +@Injectable() +export class AuditService { + private readonly memoryRecords: AuditRecordSummary[] = []; + + record(input: AuditRecordInput): void { + this.memoryRecords.push({ + ...input, + before: input.before ? redactSensitive(input.before) : undefined, + after: input.after ? redactSensitive(input.after) : undefined, + parameterDigest: input.parameterDigest + ? redactSensitive(input.parameterDigest) + : undefined, + createdAt: new Date().toISOString(), + }); + } + + getRecentRecords(): AuditRecordSummary[] { + return [...this.memoryRecords].slice(-50).reverse(); + } +} diff --git a/src/common/errors/app-error.ts b/src/common/errors/app-error.ts new file mode 100644 index 0000000..b015fd4 --- /dev/null +++ b/src/common/errors/app-error.ts @@ -0,0 +1,18 @@ +export type AppErrorCode = + | 'CONFIG_MISSING' + | 'INTEGRATION_UNAVAILABLE' + | 'INTEGRATION_REQUEST_FAILED' + | 'RESOURCE_NOT_FOUND' + | 'VALIDATION_FAILED' + | 'UNEXPECTED_ERROR'; + +export class AppError extends Error { + constructor( + public readonly code: AppErrorCode, + message: string, + public readonly statusCode = 500, + public readonly details?: Record, + ) { + super(message); + } +} diff --git a/src/common/errors/integration-error.ts b/src/common/errors/integration-error.ts new file mode 100644 index 0000000..29fb541 --- /dev/null +++ b/src/common/errors/integration-error.ts @@ -0,0 +1,25 @@ +import { AppError } from './app-error'; + +export class IntegrationUnavailableError extends AppError { + constructor(integration: string, missing: string[]) { + super( + 'INTEGRATION_UNAVAILABLE', + `${integration} integration is not configured`, + 503, + { integration, missing }, + ); + } +} + +export class IntegrationRequestError extends AppError { + constructor( + integration: string, + message: string, + details?: Record, + ) { + super('INTEGRATION_REQUEST_FAILED', message, 502, { + integration, + ...details, + }); + } +} diff --git a/src/common/http/all-exceptions.filter.ts b/src/common/http/all-exceptions.filter.ts new file mode 100644 index 0000000..e1e4a92 --- /dev/null +++ b/src/common/http/all-exceptions.filter.ts @@ -0,0 +1,82 @@ +import { + ArgumentsHost, + Catch, + ExceptionFilter, + HttpException, +} from '@nestjs/common'; +import { AppError } from '../errors/app-error'; +import { redactSensitive } from '../security/redact-sensitive'; +import { + RequestWithHeaders, + ResponseWithHeaders, + resolveRequestId, +} from './request-id'; + +type JsonResponse = ResponseWithHeaders & { + status: (statusCode: number) => { + json: (body: unknown) => unknown; + }; +}; + +@Catch() +export class AllExceptionsFilter implements ExceptionFilter { + catch(exception: unknown, host: ArgumentsHost): void { + const httpContext = host.switchToHttp(); + const request = httpContext.getRequest(); + const response = httpContext.getResponse(); + const payload = this.toPayload(exception); + const requestId = resolveRequestId(request.headers); + + response.setHeader('x-request-id', requestId); + + response.status(payload.statusCode).json({ + success: false, + error: { + code: payload.code, + message: payload.message, + details: redactSensitive(payload.details ?? {}), + }, + requestId, + timestamp: new Date().toISOString(), + }); + } + + private toPayload(exception: unknown): { + statusCode: number; + code: string; + message: string; + details?: Record; + } { + if (exception instanceof AppError) { + return { + statusCode: exception.statusCode, + code: exception.code, + message: exception.message, + details: exception.details, + }; + } + + if (exception instanceof HttpException) { + return { + statusCode: exception.getStatus(), + code: 'VALIDATION_FAILED', + message: exception.message, + details: { response: exception.getResponse() }, + }; + } + + if (exception instanceof Error) { + return { + statusCode: 500, + code: 'UNEXPECTED_ERROR', + message: exception.message, + }; + } + + return { + statusCode: 500, + code: 'UNEXPECTED_ERROR', + message: 'Unexpected error', + }; + } +} diff --git a/src/common/http/api-envelope.interceptor.ts b/src/common/http/api-envelope.interceptor.ts new file mode 100644 index 0000000..5f52ddc --- /dev/null +++ b/src/common/http/api-envelope.interceptor.ts @@ -0,0 +1,66 @@ +import { + CallHandler, + ExecutionContext, + Injectable, + NestInterceptor, +} from '@nestjs/common'; +import { Observable, map } from 'rxjs'; +import { + RequestWithHeaders, + ResponseWithHeaders, + resolveRequestId, +} from './request-id'; + +/** + * 将所有成功响应收敛成前后端共享的 enterprise envelope。 + */ +export type ApiSuccessEnvelope = { + success: true; + data: T; + requestId: string; + timestamp: string; +}; + +@Injectable() +export class ApiEnvelopeInterceptor + implements NestInterceptor> +{ + intercept( + context: ExecutionContext, + next: CallHandler, + ): Observable> { + const httpContext = context.switchToHttp(); + const request = httpContext.getRequest(); + const response = httpContext.getResponse(); + const requestId = resolveRequestId(request.headers); + + response.setHeader('x-request-id', requestId); + + return next.handle().pipe( + map((payload) => ({ + success: true as const, + data: unwrapLegacyData(payload), + requestId, + timestamp: new Date().toISOString(), + })), + ); + } +} + +function unwrapLegacyData(payload: unknown): unknown { + if (!isRecord(payload)) { + return payload; + } + + const keys = Object.keys(payload); + + if (keys.length === 1 && keys[0] === 'data') { + return payload.data; + } + + return payload; +} + +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} diff --git a/src/common/http/request-id.ts b/src/common/http/request-id.ts new file mode 100644 index 0000000..443faab --- /dev/null +++ b/src/common/http/request-id.ts @@ -0,0 +1,25 @@ +import { randomUUID } from 'node:crypto'; + +/** + * 统一提取或生成请求 ID,供响应包裹、错误处理和后续审计链路复用。 + */ +export type RequestHeaders = Record; + +export type RequestWithHeaders = { + headers?: RequestHeaders; +}; + +export type ResponseWithHeaders = { + setHeader: (name: string, value: string) => void; +}; + +export function resolveRequestId(headers?: RequestHeaders): string { + const rawRequestId = headers?.['x-request-id']; + const requestId = Array.isArray(rawRequestId) ? rawRequestId[0] : rawRequestId; + + if (requestId?.trim()) { + return requestId.trim(); + } + + return `req_${randomUUID()}`; +} diff --git a/src/common/pipes/zod-validation.pipe.ts b/src/common/pipes/zod-validation.pipe.ts new file mode 100644 index 0000000..5bd4a16 --- /dev/null +++ b/src/common/pipes/zod-validation.pipe.ts @@ -0,0 +1,32 @@ +import { Injectable, PipeTransform } from '@nestjs/common'; +import { ZodType, z } from 'zod'; +import { AppError } from '../errors/app-error'; + +/** + * 使用 Zod 做运行时入参校验,避免 DTO 只停留在 Swagger 文档层。 + */ +@Injectable() +export class ZodValidationPipe implements PipeTransform { + constructor(private readonly schema: ZodType) {} + + transform(value: unknown): T { + const result = this.schema.safeParse(value); + + if (!result.success) { + throw new AppError( + 'VALIDATION_FAILED', + 'Request body validation failed', + 400, + { + issues: result.error.issues.map((issue: z.core.$ZodIssue) => ({ + path: issue.path.join('.'), + code: issue.code, + message: issue.message, + })), + }, + ); + } + + return result.data; + } +} diff --git a/src/common/security/redact-sensitive.ts b/src/common/security/redact-sensitive.ts new file mode 100644 index 0000000..3b3a932 --- /dev/null +++ b/src/common/security/redact-sensitive.ts @@ -0,0 +1,31 @@ +const SENSITIVE_KEY_PATTERN = + /(token|secret|password|passwd|authorization|api[-_]?key|webhook|cookie|credential|private[-_]?key)/i; + +export function redactSensitive(value: T): T { + return redactValue(value) as T; +} + +function redactValue(value: unknown): unknown { + if (Array.isArray(value)) { + return value.map((item) => redactValue(item)); + } + + if (value && typeof value === 'object') { + return Object.fromEntries( + Object.entries(value as Record).map(([key, entry]) => [ + key, + SENSITIVE_KEY_PATTERN.test(key) ? '[REDACTED]' : redactValue(entry), + ]), + ); + } + + if (typeof value === 'string' && looksLikeSecret(value)) { + return '[REDACTED]'; + } + + return value; +} + +function looksLikeSecret(value: string): boolean { + return value.length >= 24 && /^[A-Za-z0-9_./+=:-]+$/.test(value); +} diff --git a/src/config/env.schema.ts b/src/config/env.schema.ts new file mode 100644 index 0000000..5d337e4 --- /dev/null +++ b/src/config/env.schema.ts @@ -0,0 +1,76 @@ +import { z } from 'zod'; + +const optionalUrl = z + .string() + .trim() + .url() + .optional() + .or(z.literal('').transform(() => undefined)); + +const optionalString = z + .string() + .trim() + .min(1) + .optional() + .or(z.literal('').transform(() => undefined)); + +const booleanFromEnv = z.preprocess((value) => { + if (value === undefined || value === null || value === '') { + return undefined; + } + + if (typeof value === 'boolean') { + return value; + } + + if (typeof value === 'string') { + return ['1', 'true', 'yes', 'on'].includes(value.toLowerCase()); + } + + if (typeof value === 'number') { + return value === 1; + } + + return false; +}, z.boolean().default(false)); + +export const envSchema = z.object({ + NODE_ENV: z.enum(['development', 'test', 'production']).default('development'), + PORT: z.coerce.number().int().positive().default(4300), + CORS_ORIGIN: optionalString, + LOG_LEVEL: z.enum(['fatal', 'error', 'warn', 'info', 'debug', 'trace']).default('info'), + DATABASE_URL: optionalString, + USE_DATABASE_READS: booleanFromEnv, + PRISMA_CONNECT_ON_BOOT: booleanFromEnv, + REDIS_URL: optionalString, + JENKINS_BASE_URL: optionalUrl, + JENKINS_USERNAME: optionalString, + JENKINS_API_TOKEN: optionalString, + GITEA_BASE_URL: optionalUrl, + GITEA_TOKEN: optionalString, + GITEA_WEBHOOK_SECRET: optionalString, + DEVOPS_PUBLIC_URL: optionalUrl, + NOTIFICATION_PROVIDER: optionalString, + NOTIFICATION_WEBHOOK_URL: optionalUrl, + FEISHU_WEBHOOK_URL: optionalUrl, + WECOM_WEBHOOK_URL: optionalUrl, + LLM_BASE_URL: optionalUrl, + LLM_API_KEY: optionalString, + LLM_MODEL: optionalString, + SECRET_ENCRYPTION_KEY: optionalString, +}); + +export type EnvConfig = z.infer; + +export function validateEnv(config: Record): EnvConfig { + const result = envSchema.safeParse(config); + + if (!result.success) { + const details = result.error.issues + .map((issue) => `${issue.path.join('.')}: ${issue.message}`) + .join('; '); + throw new Error(`Invalid environment configuration: ${details}`); + } + + return result.data; +} diff --git a/src/deploy-runs/create-deploy-run.dto.ts b/src/deploy-runs/create-deploy-run.dto.ts new file mode 100644 index 0000000..04ecd33 --- /dev/null +++ b/src/deploy-runs/create-deploy-run.dto.ts @@ -0,0 +1,45 @@ +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { z } from 'zod'; +import { DeployEnvironment } from './deploy-run.types'; + +const optionalTrimmedString = (maxLength: number) => + z + .string() + .trim() + .max(maxLength) + .optional() + .or(z.literal('').transform(() => undefined)); + +/** + * 创建发布单的运行时校验规则;真实密钥和第三方参数不允许从前端传入。 + */ +export const createDeployRunSchema = z.object({ + projectKey: z.string().trim().min(1).max(80), + environment: z.enum(['test', 'production']), + ref: z.string().trim().min(1).max(200), + operator: z.string().trim().min(1).max(120), + remark: optionalTrimmedString(500), + idempotencyKey: optionalTrimmedString(260), +}); + +export class CreateDeployRunDto { + @ApiProperty({ example: 'access-manage' }) + projectKey!: string; + + @ApiProperty({ enum: ['test', 'production'], example: 'test' }) + environment!: DeployEnvironment; + + @ApiProperty({ example: 'develop' }) + ref!: string; + + @ApiProperty({ example: 'operator@example.com' }) + operator!: string; + + @ApiPropertyOptional({ example: 'Manual test deploy before release.' }) + remark?: string; + + @ApiPropertyOptional({ + example: 'access-manage:test:develop:2026-06-11T10:00:00Z', + }) + idempotencyKey?: string; +} diff --git a/src/deploy-runs/deploy-execution.service.spec.ts b/src/deploy-runs/deploy-execution.service.spec.ts new file mode 100644 index 0000000..b11abfd --- /dev/null +++ b/src/deploy-runs/deploy-execution.service.spec.ts @@ -0,0 +1,251 @@ +import { AuditService } from '../audit/audit.service'; +import { JenkinsClient } from '../integrations/jenkins/jenkins.client'; +import { DeployNotificationService } from '../notifications/wecom/deploy-notification.service'; +import { ProjectSummary } from '../projects/project.types'; +import { DeployExecutionService } from './deploy-execution.service'; +import { DeployRunRepository } from './deploy-run.repository'; +import { CreateDeployRunInput, DeployRunSummary } from './deploy-run.types'; + +describe('DeployExecutionService', () => { + const project: ProjectSummary = { + id: 'project_001', + key: 'access-manage', + name: 'access-manage', + repositoryUrl: 'https://gitea.ops.mrzhan.top/my-project/access-manage.git', + defaultBranch: 'develop', + status: 'active', + environments: [ + { + name: 'test', + displayName: '测试环境', + jenkinsJobPath: 'my-project-dev/test-access-manage-develop', + }, + ], + }; + const input: CreateDeployRunInput = { + projectKey: 'access-manage', + environment: 'test', + ref: 'develop', + operator: 'ops-admin', + }; + + function createRepository(): DeployRunRepository { + return new DeployRunRepository( + { + get: jest.fn(() => false), + } as never, + {} as never, + ); + } + + function createService(options: { + jenkinsHealth: ReturnType; + notificationStatus: 'sent' | 'skipped'; + queueId?: string; + queueBuildNumber?: number; + buildResult?: 'SUCCESS' | 'FAILURE' | 'ABORTED' | 'UNSTABLE'; + buildRunning?: boolean; + logText?: string; + }) { + const repository = createRepository(); + const audit = { + record: jest.fn(), + } satisfies Pick; + const notification = { + sendDeployRunEvent: jest.fn(() => + Promise.resolve({ + status: options.notificationStatus, + channel: 'wecom' as const, + reason: + options.notificationStatus === 'skipped' + ? 'Missing WECOM_WEBHOOK_URL' + : undefined, + }), + ), + } satisfies Pick; + const jenkins = { + healthSummary: jest.fn(() => options.jenkinsHealth), + buildWithParameters: jest.fn(() => + Promise.resolve({ + queueId: options.queueId ?? 'queue_001', + }), + ), + getQueueItem: jest.fn((queueId: string) => + Promise.resolve({ + queueId, + buildNumber: options.queueBuildNumber, + executableUrl: options.queueBuildNumber + ? `https://jenkins.example/job/${options.queueBuildNumber}/` + : undefined, + }), + ), + getBuildStatus: jest.fn((_jobPath: string, buildNumber: number) => + Promise.resolve({ + jobPath: 'my-project-dev/test-access-manage-develop', + buildNumber, + building: options.buildRunning ?? false, + result: options.buildResult, + url: `https://jenkins.example/job/${buildNumber}/`, + }), + ), + getProgressiveText: jest.fn(() => + Promise.resolve({ + text: options.logText ?? '', + nextStart: options.logText?.length ?? 0, + hasMore: false, + }), + ), + } satisfies Pick< + JenkinsClient, + | 'healthSummary' + | 'buildWithParameters' + | 'getQueueItem' + | 'getBuildStatus' + | 'getProgressiveText' + >; + const service = new DeployExecutionService( + repository, + audit as unknown as AuditService, + notification as unknown as DeployNotificationService, + jenkins as unknown as JenkinsClient, + ); + + return { + audit, + jenkins, + notification, + repository, + service, + }; + } + + function createRun( + repository: DeployRunRepository, + idempotencyKey: string, + ): DeployRunSummary { + return repository.create({ + ...input, + idempotencyKey, + }); + } + + function step(run: DeployRunSummary, bpmnNodeId: string) { + return run.steps.find((item) => item.bpmnNodeId === bpmnNodeId); + } + + it('completes a dry-run release when Jenkins is not configured', async () => { + const { audit, jenkins, repository, service } = createService({ + jenkinsHealth: { + status: 'not_configured', + message: 'Missing JENKINS_API_TOKEN', + }, + notificationStatus: 'skipped', + }); + const run = createRun(repository, 'run_dry_001'); + + const result = await service.execute(run, project); + + expect(result.status).toBe('success'); + expect(result.jenkinsQueueId).toBeUndefined(); + expect(step(result, 'gitea-check')?.status).toBe('success'); + expect(step(result, 'jenkins-build')?.status).toBe('success'); + expect(step(result, 'health-check')?.status).toBe('success'); + expect(step(result, 'notify-wecom')?.status).toBe('skipped'); + expect(step(result, 'finish-release')?.status).toBe('success'); + expect(jenkins.buildWithParameters).not.toHaveBeenCalled(); + expect(audit.record).toHaveBeenCalledWith( + expect.objectContaining({ action: 'JENKINS_DRY_RUN_COMPLETED' }), + ); + expect(audit.record).toHaveBeenCalledWith( + expect.objectContaining({ action: 'DEPLOY_SUCCEEDED' }), + ); + }); + + it('triggers Jenkins and keeps the run queued until status polling completes', async () => { + const { audit, jenkins, repository, service } = createService({ + jenkinsHealth: { status: 'ok' }, + notificationStatus: 'sent', + queueId: 'jenkins_queue_42', + }); + const run = createRun(repository, 'run_queue_001'); + + const result = await service.execute(run, project); + + expect(result.status).toBe('queued'); + expect(result.jenkinsQueueId).toBe('jenkins_queue_42'); + expect(step(result, 'jenkins-build')?.status).toBe('queued'); + expect(step(result, 'health-check')?.status).toBe('pending'); + expect(step(result, 'notify-wecom')?.status).toBe('pending'); + expect(jenkins.buildWithParameters).toHaveBeenCalledWith( + 'my-project-dev/test-access-manage-develop', + expect.objectContaining({ + DEPLOY_RUN_ID: 'run_queue_001', + DEPLOY_ENVIRONMENT: 'test', + GIT_REF: 'develop', + }), + ); + expect(audit.record).toHaveBeenCalledWith( + expect.objectContaining({ + action: 'JENKINS_BUILD_TRIGGERED', + after: expect.objectContaining({ + queueId: 'jenkins_queue_42', + }), + }), + ); + expect(audit.record).not.toHaveBeenCalledWith( + expect.objectContaining({ action: 'DEPLOY_SUCCEEDED' }), + ); + }); + + it('syncs a queued Jenkins run to success', async () => { + const { audit, repository, service } = createService({ + jenkinsHealth: { status: 'ok' }, + notificationStatus: 'sent', + queueId: 'queue_success', + queueBuildNumber: 18, + buildResult: 'SUCCESS', + }); + const run = await service.execute(createRun(repository, 'run_success_001'), project); + + const result = await service.syncJenkinsRun(run, project); + + expect(result.status).toBe('success'); + expect(result.jenkinsBuildNumber).toBe(18); + expect(step(result, 'jenkins-build')?.status).toBe('success'); + expect(step(result, 'health-check')?.status).toBe('success'); + expect(step(result, 'notify-wecom')?.status).toBe('success'); + expect(step(result, 'finish-release')?.status).toBe('success'); + expect(audit.record).toHaveBeenCalledWith( + expect.objectContaining({ action: 'JENKINS_BUILD_STARTED' }), + ); + expect(audit.record).toHaveBeenCalledWith( + expect.objectContaining({ action: 'DEPLOY_SUCCEEDED' }), + ); + }); + + it('syncs a queued Jenkins run to failure with a log excerpt', async () => { + const { audit, repository, service } = createService({ + jenkinsHealth: { status: 'ok' }, + notificationStatus: 'skipped', + queueId: 'queue_failure', + queueBuildNumber: 19, + buildResult: 'FAILURE', + logText: ['install ok', 'build failed', 'token=secret-value'].join('\n'), + }); + const run = await service.execute(createRun(repository, 'run_failure_001'), project); + + const result = await service.syncJenkinsRun(run, project); + + expect(result.status).toBe('failed'); + expect(result.failureSummary).toBe('Jenkins build #19 FAILURE'); + expect(step(result, 'jenkins-build')?.status).toBe('failed'); + expect(step(result, 'jenkins-build')?.errorSummary).toBe( + 'Jenkins build #19 FAILURE', + ); + expect(step(result, 'jenkins-build')?.logExcerpt).toContain('build failed'); + expect(step(result, 'notify-wecom')?.status).toBe('skipped'); + expect(audit.record).toHaveBeenCalledWith( + expect.objectContaining({ action: 'DEPLOY_FAILED' }), + ); + }); +}); diff --git a/src/deploy-runs/deploy-execution.service.ts b/src/deploy-runs/deploy-execution.service.ts new file mode 100644 index 0000000..0a827cd --- /dev/null +++ b/src/deploy-runs/deploy-execution.service.ts @@ -0,0 +1,541 @@ +import { Injectable } from '@nestjs/common'; +import { AuditService } from '../audit/audit.service'; +import { AppError } from '../common/errors/app-error'; +import { redactSensitive } from '../common/security/redact-sensitive'; +import { JenkinsClient } from '../integrations/jenkins/jenkins.client'; +import { JenkinsBuildStatus } from '../integrations/jenkins/jenkins.types'; +import { + DeployNotificationEvent, + DeployNotificationService, +} from '../notifications/wecom/deploy-notification.service'; +import { + ProjectEnvironmentSummary, + ProjectSummary, +} from '../projects/project.types'; +import { DeployRunRepository } from './deploy-run.repository'; +import { DeployRunStepStatus, DeployRunSummary } from './deploy-run.types'; + +@Injectable() +export class DeployExecutionService { + constructor( + private readonly deployRunRepository: DeployRunRepository, + private readonly auditService: AuditService, + private readonly deployNotificationService: DeployNotificationService, + private readonly jenkinsClient: JenkinsClient, + ) {} + + async execute( + run: DeployRunSummary, + project: ProjectSummary, + ): Promise { + if (run.status !== 'pending') { + return run; + } + + let currentRun = this.requireRun( + run.id, + this.deployRunRepository.updateMemoryRun(run.id, { status: 'running' }), + ); + let currentStep: string | undefined; + + this.auditService.record({ + action: 'DEPLOY_EXECUTION_STARTED', + resourceType: 'deploy_run', + resourceId: currentRun.id, + actorName: currentRun.operator, + after: { + projectKey: currentRun.projectKey, + environment: currentRun.environment, + ref: currentRun.ref, + status: currentRun.status, + }, + }); + + try { + currentStep = 'gitea-check'; + currentRun = this.completeStep(currentRun.id, currentStep, 'success'); + + currentStep = 'jenkins-build'; + const jenkinsResult = await this.advanceJenkinsBuild(currentRun, project); + currentRun = jenkinsResult.run; + + if (!jenkinsResult.shouldContinue) { + await this.recordNotificationResult(currentRun, 'queued'); + return currentRun; + } + + currentStep = 'health-check'; + currentRun = this.completeStep(currentRun.id, currentStep, 'success'); + + currentStep = 'notify-wecom'; + currentRun = await this.advanceNotification(currentRun, 'succeeded'); + + currentStep = 'finish-release'; + currentRun = this.completeStep(currentRun.id, currentStep, 'success'); + currentRun = this.requireRun( + currentRun.id, + this.deployRunRepository.updateMemoryRun(currentRun.id, { + status: 'success', + }), + ); + + this.auditService.record({ + action: 'DEPLOY_SUCCEEDED', + resourceType: 'deploy_run', + resourceId: currentRun.id, + actorName: currentRun.operator, + after: { + status: currentRun.status, + jenkinsQueueId: currentRun.jenkinsQueueId, + jenkinsBuildNumber: currentRun.jenkinsBuildNumber, + }, + }); + + return currentRun; + } catch (error) { + const failureSummary = this.toFailureSummary(error); + + if (currentStep) { + currentRun = + this.deployRunRepository.updateMemoryStep( + currentRun.id, + currentStep, + { + status: 'failed', + errorSummary: failureSummary, + }, + ) ?? currentRun; + } + + currentRun = + this.deployRunRepository.updateMemoryRun(currentRun.id, { + status: 'failed', + failureSummary, + }) ?? currentRun; + + currentRun = await this.advanceNotification(currentRun, 'failed'); + + this.auditService.record({ + action: 'DEPLOY_FAILED', + resourceType: 'deploy_run', + resourceId: currentRun.id, + actorName: currentRun.operator, + after: { + status: currentRun.status, + failedStep: currentStep, + failureSummary, + }, + }); + + return currentRun; + } + } + + async syncJenkinsRun( + run: DeployRunSummary, + project: ProjectSummary, + ): Promise { + if (['success', 'failed', 'canceled'].includes(run.status)) { + return run; + } + + if (!run.jenkinsQueueId && !run.jenkinsBuildNumber) { + return run; + } + + const environment = this.findEnvironment(project, run); + let currentRun = run; + let buildNumber = currentRun.jenkinsBuildNumber; + + if (!buildNumber && currentRun.jenkinsQueueId) { + const queueItem = await this.jenkinsClient.getQueueItem( + currentRun.jenkinsQueueId, + ); + + if (!queueItem.buildNumber) { + return this.requireRun( + currentRun.id, + this.deployRunRepository.updateMemoryRun(currentRun.id, { + status: 'queued', + jenkinsQueueId: queueItem.queueId, + jenkinsBuildUrl: queueItem.executableUrl, + }), + ); + } + + buildNumber = queueItem.buildNumber; + currentRun = this.requireRun( + currentRun.id, + this.deployRunRepository.updateMemoryRun(currentRun.id, { + status: 'running', + jenkinsQueueId: queueItem.queueId, + jenkinsBuildNumber: queueItem.buildNumber, + jenkinsBuildUrl: queueItem.executableUrl, + }), + ); + currentRun = this.markStep( + currentRun.id, + 'jenkins-build', + 'running', + `Jenkins build #${queueItem.buildNumber} is running.`, + ); + + this.auditService.record({ + action: 'JENKINS_BUILD_STARTED', + resourceType: 'deploy_run', + resourceId: currentRun.id, + actorName: currentRun.operator, + after: { + queueId: queueItem.queueId, + buildNumber: queueItem.buildNumber, + buildUrl: queueItem.executableUrl, + }, + }); + } + + if (!buildNumber) { + return currentRun; + } + + const buildStatus = await this.jenkinsClient.getBuildStatus( + environment.jenkinsJobPath, + buildNumber, + ); + + currentRun = this.requireRun( + currentRun.id, + this.deployRunRepository.updateMemoryRun(currentRun.id, { + status: buildStatus.building || !buildStatus.result ? 'running' : currentRun.status, + jenkinsBuildNumber: buildStatus.buildNumber, + jenkinsBuildUrl: buildStatus.url, + }), + ); + + if (buildStatus.building || !buildStatus.result) { + return this.markStep( + currentRun.id, + 'jenkins-build', + 'running', + `Jenkins build #${buildStatus.buildNumber} is running.`, + ); + } + + if (buildStatus.result === 'SUCCESS') { + return this.completeSuccessfulJenkinsRun(currentRun); + } + + return this.failJenkinsRun( + currentRun, + buildStatus.result, + environment.jenkinsJobPath, + ); + } + + private async advanceJenkinsBuild( + run: DeployRunSummary, + project: ProjectSummary, + ): Promise<{ run: DeployRunSummary; shouldContinue: boolean }> { + const environment = this.findEnvironment(project, run); + const health = this.jenkinsClient.healthSummary(); + + if (health.status === 'not_configured') { + const reason = + health.message ?? + 'Jenkins is not configured; simulated build completed.'; + const currentRun = this.completeStep(run.id, 'jenkins-build', 'success'); + + this.auditService.record({ + action: 'JENKINS_DRY_RUN_COMPLETED', + resourceType: 'deploy_run', + resourceId: run.id, + actorName: run.operator, + after: { + mode: 'dry-run', + reason, + jobPath: environment.jenkinsJobPath, + }, + }); + + return { run: currentRun, shouldContinue: true }; + } + + if (!environment.jenkinsJobPath) { + throw new AppError( + 'VALIDATION_FAILED', + `Project ${project.key} ${run.environment} Jenkins job path is empty`, + 400, + { projectKey: project.key, environment: run.environment }, + ); + } + + this.markStep(run.id, 'jenkins-build', 'running'); + const queueItem = await this.jenkinsClient.buildWithParameters( + environment.jenkinsJobPath, + this.buildParameters(run), + ); + const nextStatus = queueItem.buildNumber ? 'running' : 'queued'; + let currentRun = this.requireRun( + run.id, + this.deployRunRepository.updateMemoryRun(run.id, { + status: nextStatus, + jenkinsQueueId: queueItem.queueId, + jenkinsBuildNumber: queueItem.buildNumber, + jenkinsBuildUrl: queueItem.executableUrl, + }), + ); + currentRun = this.markStep( + run.id, + 'jenkins-build', + nextStatus, + queueItem.buildNumber + ? `Jenkins build #${queueItem.buildNumber} is running.` + : `Jenkins queue ${queueItem.queueId} accepted.`, + ); + + this.auditService.record({ + action: 'JENKINS_BUILD_TRIGGERED', + resourceType: 'deploy_run', + resourceId: run.id, + actorName: run.operator, + after: { + jobPath: environment.jenkinsJobPath, + queueId: queueItem.queueId, + buildNumber: queueItem.buildNumber, + }, + parameterDigest: this.buildParameters(run), + }); + + return { run: currentRun, shouldContinue: false }; + } + + private async advanceNotification( + run: DeployRunSummary, + event: DeployNotificationEvent, + ): Promise { + this.markStep(run.id, 'notify-wecom', 'running'); + const notification = + await this.deployNotificationService.sendDeployRunEvent(run, event); + const stepStatus = + notification.status === 'sent' + ? 'success' + : notification.status === 'failed' + ? 'failed' + : 'skipped'; + const currentRun = this.completeStep( + run.id, + 'notify-wecom', + stepStatus, + notification.status === 'sent' ? undefined : notification.reason, + ); + + this.auditService.record({ + action: `WECOM_NOTIFICATION_${notification.status.toUpperCase()}`, + resourceType: 'deploy_run', + resourceId: run.id, + actorName: run.operator, + after: { + event, + channel: notification.channel, + reason: notification.reason, + }, + }); + + return currentRun; + } + + private async completeSuccessfulJenkinsRun( + run: DeployRunSummary, + ): Promise { + let currentRun = this.completeStep(run.id, 'jenkins-build', 'success'); + currentRun = this.completeStep(currentRun.id, 'health-check', 'success'); + currentRun = await this.advanceNotification(currentRun, 'succeeded'); + currentRun = this.completeStep(currentRun.id, 'finish-release', 'success'); + currentRun = this.requireRun( + currentRun.id, + this.deployRunRepository.updateMemoryRun(currentRun.id, { + status: 'success', + }), + ); + + this.auditService.record({ + action: 'DEPLOY_SUCCEEDED', + resourceType: 'deploy_run', + resourceId: currentRun.id, + actorName: currentRun.operator, + after: { + status: currentRun.status, + jenkinsQueueId: currentRun.jenkinsQueueId, + jenkinsBuildNumber: currentRun.jenkinsBuildNumber, + }, + }); + + return currentRun; + } + + private async failJenkinsRun( + run: DeployRunSummary, + result: Exclude, 'SUCCESS'>, + jobPath: string, + ): Promise { + const logExcerpt = await this.readFailureLogExcerpt(run, jobPath); + const failureSummary = `Jenkins build #${run.jenkinsBuildNumber ?? '-'} ${result}`; + let currentRun = + this.deployRunRepository.updateMemoryStep(run.id, 'jenkins-build', { + status: 'failed', + errorSummary: failureSummary, + logExcerpt, + }) ?? run; + currentRun = + this.deployRunRepository.updateMemoryRun(currentRun.id, { + status: 'failed', + failureSummary, + }) ?? currentRun; + currentRun = await this.advanceNotification(currentRun, 'failed'); + + this.auditService.record({ + action: 'DEPLOY_FAILED', + resourceType: 'deploy_run', + resourceId: currentRun.id, + actorName: currentRun.operator, + after: { + status: currentRun.status, + failedStep: 'jenkins-build', + failureSummary, + }, + }); + + return currentRun; + } + + private async readFailureLogExcerpt( + run: DeployRunSummary, + jobPath: string, + ): Promise { + if (!run.jenkinsBuildNumber) { + return undefined; + } + + try { + const log = await this.jenkinsClient.getProgressiveText( + jobPath, + run.jenkinsBuildNumber, + ); + const logExcerpt = log.text + .split(/\r?\n/) + .filter(Boolean) + .slice(-24) + .join('\n') + .slice(-4000); + + return redactSensitive({ logExcerpt }).logExcerpt; + } catch { + return undefined; + } + } + + private findEnvironment( + project: ProjectSummary, + run: DeployRunSummary, + ): ProjectEnvironmentSummary { + const environment = project.environments.find( + (item) => item.name === run.environment, + ); + + if (!environment) { + throw new AppError( + 'VALIDATION_FAILED', + `Project ${project.key} does not expose ${run.environment} environment`, + 400, + { projectKey: project.key, environment: run.environment }, + ); + } + + return environment; + } + + private buildParameters(run: DeployRunSummary): Record { + return { + DEPLOY_RUN_ID: run.id, + PROJECT_KEY: run.projectKey, + DEPLOY_ENVIRONMENT: run.environment, + GIT_REF: run.ref, + OPERATOR: run.operator, + }; + } + + private completeStep( + runId: string, + bpmnNodeId: string, + status: Extract, + errorSummary?: string, + ): DeployRunSummary { + return this.requireRun( + runId, + this.deployRunRepository.updateMemoryStep(runId, bpmnNodeId, { + status, + errorSummary, + }), + ); + } + + private markStep( + runId: string, + bpmnNodeId: string, + status: Extract, + message?: string, + ): DeployRunSummary { + return this.requireRun( + runId, + this.deployRunRepository.updateMemoryStep(runId, bpmnNodeId, { + status, + message, + }), + ); + } + + private async recordNotificationResult( + run: DeployRunSummary, + event: DeployNotificationEvent, + ): Promise { + const notification = + await this.deployNotificationService.sendDeployRunEvent(run, event); + + this.auditService.record({ + action: `WECOM_NOTIFICATION_${notification.status.toUpperCase()}`, + resourceType: 'deploy_run', + resourceId: run.id, + actorName: run.operator, + after: { + event, + channel: notification.channel, + reason: notification.reason, + }, + }); + } + + private requireRun( + id: string, + run: DeployRunSummary | null, + ): DeployRunSummary { + if (!run) { + throw new AppError( + 'RESOURCE_NOT_FOUND', + `Deploy run ${id} not found`, + 404, + { + id, + }, + ); + } + + return run; + } + + private toFailureSummary(error: unknown): string { + if (error instanceof Error) { + return redactSensitive({ message: error.message }).message; + } + + return 'Unknown deploy execution error'; + } +} diff --git a/src/deploy-runs/deploy-run.repository.ts b/src/deploy-runs/deploy-run.repository.ts new file mode 100644 index 0000000..2b0e39c --- /dev/null +++ b/src/deploy-runs/deploy-run.repository.ts @@ -0,0 +1,341 @@ +import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { EnvConfig } from '../config/env.schema'; +import { PrismaService } from '../prisma/prisma.service'; +import { seedDeployRuns } from './deploy-run.seed'; +import { + CreateDeployRunInput, + DeployRunStepStatus, + DeployRunSummary, + DeployRunStatus, +} from './deploy-run.types'; + +type DeployRunMemoryPatch = { + status?: DeployRunStatus; + jenkinsQueueId?: string; + jenkinsBuildNumber?: number; + jenkinsBuildUrl?: string; + failureSummary?: string; + startedAt?: string; + finishedAt?: string; +}; + +type DeployRunStepMemoryPatch = { + status?: DeployRunStepStatus; + startedAt?: string; + finishedAt?: string; + message?: string; + errorSummary?: string; + logExcerpt?: string; +}; + +const terminalRunStatuses = new Set([ + 'success', + 'failed', + 'canceled', +]); +const terminalStepStatuses = new Set([ + 'success', + 'failed', + 'canceled', + 'skipped', +]); + +@Injectable() +export class DeployRunRepository { + private readonly memoryRuns = [...seedDeployRuns]; + + constructor( + private readonly config: ConfigService, + private readonly prisma: PrismaService, + ) {} + + async findMany(): Promise { + if (!this.useDatabaseReads()) { + return [...this.memoryRuns].sort((left, right) => + right.createdAt.localeCompare(left.createdAt), + ); + } + + const runs = await this.prisma.deployRun.findMany({ + include: { + project: true, + environment: true, + steps: { + orderBy: { + order: 'asc', + }, + }, + }, + orderBy: { + createdAt: 'desc', + }, + take: 50, + }); + + return runs.map((run) => ({ + id: run.id, + projectKey: run.project.key, + environment: run.environment.name === 'TEST' ? 'test' : 'production', + ref: run.ref, + status: run.status.toLowerCase() as DeployRunStatus, + trigger: run.trigger.toLowerCase() as DeployRunSummary['trigger'], + operator: run.operator, + jenkinsQueueId: run.jenkinsQueueId ?? undefined, + jenkinsBuildNumber: run.jenkinsBuildNumber ?? undefined, + failureSummary: run.failureSummary ?? undefined, + startedAt: run.startedAt?.toISOString(), + finishedAt: run.finishedAt?.toISOString(), + createdAt: run.createdAt.toISOString(), + steps: run.steps.map((step) => ({ + id: step.id, + bpmnNodeId: step.bpmnNodeId, + name: step.name, + status: step.status.toLowerCase() as DeployRunStepStatus, + order: step.order, + startedAt: step.startedAt?.toISOString(), + finishedAt: step.finishedAt?.toISOString(), + errorSummary: step.errorSummary ?? undefined, + })), + })); + } + + async findById(id: string): Promise { + if (!this.useDatabaseReads()) { + return this.memoryRuns.find((run) => run.id === id) ?? null; + } + + const run = await this.prisma.deployRun.findUnique({ + where: { id }, + include: { + project: true, + environment: true, + steps: { + orderBy: { + order: 'asc', + }, + }, + }, + }); + + if (!run) { + return null; + } + + return { + id: run.id, + projectKey: run.project.key, + environment: run.environment.name === 'TEST' ? 'test' : 'production', + ref: run.ref, + status: run.status.toLowerCase() as DeployRunStatus, + trigger: run.trigger.toLowerCase() as DeployRunSummary['trigger'], + operator: run.operator, + jenkinsQueueId: run.jenkinsQueueId ?? undefined, + jenkinsBuildNumber: run.jenkinsBuildNumber ?? undefined, + failureSummary: run.failureSummary ?? undefined, + startedAt: run.startedAt?.toISOString(), + finishedAt: run.finishedAt?.toISOString(), + createdAt: run.createdAt.toISOString(), + steps: run.steps.map((step) => ({ + id: step.id, + bpmnNodeId: step.bpmnNodeId, + name: step.name, + status: step.status.toLowerCase() as DeployRunStepStatus, + order: step.order, + startedAt: step.startedAt?.toISOString(), + finishedAt: step.finishedAt?.toISOString(), + errorSummary: step.errorSummary ?? undefined, + })), + }; + } + + create(input: CreateDeployRunInput): DeployRunSummary { + const idempotencyKey = + input.idempotencyKey ?? + [ + input.projectKey, + input.environment, + input.ref, + input.operator, + new Date().toISOString(), + ].join(':'); + const existing = this.memoryRuns.find((run) => run.id === idempotencyKey); + + if (existing) { + return existing; + } + + const now = new Date().toISOString(); + const run: DeployRunSummary = { + id: idempotencyKey, + projectKey: input.projectKey, + environment: input.environment, + ref: input.ref, + status: 'pending', + trigger: input.trigger ?? 'manual', + operator: input.operator, + createdAt: now, + steps: [ + { + id: `${idempotencyKey}:start-release`, + bpmnNodeId: 'start-release', + name: '提交发布单', + status: 'success', + order: 10, + }, + { + id: `${idempotencyKey}:gitea-check`, + bpmnNodeId: 'gitea-check', + name: '校验 Gitea ref', + status: 'pending', + order: 20, + }, + { + id: `${idempotencyKey}:jenkins-build`, + bpmnNodeId: 'jenkins-build', + name: '触发 Jenkins 构建', + status: 'pending', + order: 30, + }, + { + id: `${idempotencyKey}:health-check`, + bpmnNodeId: 'health-check', + name: '环境健康检查', + status: 'pending', + order: 40, + }, + { + id: `${idempotencyKey}:notify-wecom`, + bpmnNodeId: 'notify-wecom', + name: '发送通知', + status: 'pending', + order: 90, + }, + { + id: `${idempotencyKey}:finish-release`, + bpmnNodeId: 'finish-release', + name: '归档审计', + status: 'pending', + order: 100, + }, + ], + }; + + this.memoryRuns.unshift(run); + return run; + } + + updateMemoryStatus( + id: string, + status: DeployRunStatus, + ): DeployRunSummary | null { + return this.updateMemoryRun(id, { status }); + } + + updateMemoryRun( + id: string, + patch: DeployRunMemoryPatch, + ): DeployRunSummary | null { + const run = this.memoryRuns.find((item) => item.id === id); + + if (!run) { + return null; + } + + const now = new Date().toISOString(); + + if (patch.status) { + run.status = patch.status; + + if (patch.status !== 'pending' && !run.startedAt) { + run.startedAt = patch.startedAt ?? now; + } + + if (terminalRunStatuses.has(patch.status)) { + run.finishedAt = patch.finishedAt ?? now; + } + } + + if ('jenkinsQueueId' in patch) { + run.jenkinsQueueId = patch.jenkinsQueueId; + } + + if ('jenkinsBuildNumber' in patch) { + run.jenkinsBuildNumber = patch.jenkinsBuildNumber; + } + + if ('jenkinsBuildUrl' in patch) { + run.jenkinsBuildUrl = patch.jenkinsBuildUrl; + } + + if ('failureSummary' in patch) { + run.failureSummary = patch.failureSummary; + } + + if ('startedAt' in patch) { + run.startedAt = patch.startedAt; + } + + if ('finishedAt' in patch) { + run.finishedAt = patch.finishedAt; + } + + return run; + } + + updateMemoryStep( + runId: string, + bpmnNodeId: string, + patch: DeployRunStepMemoryPatch, + ): DeployRunSummary | null { + const run = this.memoryRuns.find((item) => item.id === runId); + const step = run?.steps.find((item) => item.bpmnNodeId === bpmnNodeId); + + if (!run || !step) { + return null; + } + + const now = new Date().toISOString(); + + if (patch.status) { + step.status = patch.status; + + if (patch.status !== 'pending' && !step.startedAt) { + step.startedAt = patch.startedAt ?? now; + } + + if (terminalStepStatuses.has(patch.status)) { + step.finishedAt = patch.finishedAt ?? now; + } + } + + if ('startedAt' in patch) { + step.startedAt = patch.startedAt; + } + + if ('finishedAt' in patch) { + step.finishedAt = patch.finishedAt; + } + + if ('message' in patch) { + step.message = patch.message; + } + + if ('errorSummary' in patch) { + step.errorSummary = patch.errorSummary; + } + + if ('logExcerpt' in patch) { + step.logExcerpt = patch.logExcerpt; + } + + return run; + } + + private useDatabaseReads(): boolean { + return ( + this.config.get('USE_DATABASE_READS', { infer: true }) && + Boolean(this.config.get('DATABASE_URL', { infer: true })) + ); + } +} diff --git a/src/deploy-runs/deploy-run.seed.ts b/src/deploy-runs/deploy-run.seed.ts new file mode 100644 index 0000000..d570709 --- /dev/null +++ b/src/deploy-runs/deploy-run.seed.ts @@ -0,0 +1,60 @@ +import { DeployRunSummary } from './deploy-run.types'; + +const now = new Date().toISOString(); + +export const seedDeployRuns: DeployRunSummary[] = [ + { + id: 'run_seed_access_manage_test_001', + projectKey: 'access-manage', + environment: 'test', + ref: 'develop', + status: 'pending', + trigger: 'manual', + operator: 'system-seed', + createdAt: now, + steps: [ + { + id: 'step_seed_access_manage_start', + bpmnNodeId: 'start-release', + name: '提交发布单', + status: 'success', + order: 10, + }, + { + id: 'step_seed_access_manage_gitea', + bpmnNodeId: 'gitea-check', + name: '校验 Gitea ref', + status: 'pending', + order: 20, + }, + { + id: 'step_seed_access_manage_jenkins', + bpmnNodeId: 'jenkins-build', + name: '触发 Jenkins 构建', + status: 'pending', + order: 30, + }, + { + id: 'step_seed_access_manage_health', + bpmnNodeId: 'health-check', + name: '环境健康检查', + status: 'pending', + order: 40, + }, + { + id: 'step_seed_access_manage_notify', + bpmnNodeId: 'notify-wecom', + name: '发送通知', + status: 'pending', + order: 90, + }, + { + id: 'step_seed_access_manage_archive', + bpmnNodeId: 'finish-release', + name: '归档审计', + status: 'pending', + order: 100, + }, + ], + }, +]; diff --git a/src/deploy-runs/deploy-run.types.ts b/src/deploy-runs/deploy-run.types.ts new file mode 100644 index 0000000..43f9424 --- /dev/null +++ b/src/deploy-runs/deploy-run.types.ts @@ -0,0 +1,63 @@ +export type DeployEnvironment = 'test' | 'production'; +export type DeployRunStatus = + | 'pending' + | 'queued' + | 'running' + | 'success' + | 'failed' + | 'canceled'; +export type DeployRunStepStatus = DeployRunStatus | 'skipped'; + +export type DeployRunStepSummary = { + id: string; + bpmnNodeId: string; + name: string; + status: DeployRunStepStatus; + order: number; + startedAt?: string; + finishedAt?: string; + message?: string; + errorSummary?: string; + logExcerpt?: string; +}; + +export type DeployRunSummary = { + id: string; + projectKey: string; + environment: DeployEnvironment; + ref: string; + status: DeployRunStatus; + trigger: 'manual' | 'gitea_webhook' | 'retry'; + operator: string; + jenkinsQueueId?: string; + jenkinsBuildNumber?: number; + jenkinsBuildUrl?: string; + failureSummary?: string; + startedAt?: string; + finishedAt?: string; + createdAt: string; + steps: DeployRunStepSummary[]; +}; + +export type CreateDeployRunInput = { + projectKey: string; + environment: DeployEnvironment; + ref: string; + operator: string; + remark?: string; + idempotencyKey?: string; + trigger?: DeployRunSummary['trigger']; +}; + +export type DeployRunJenkinsSyncError = { + id: string; + projectKey: string; + message: string; +}; + +export type DeployRunJenkinsSyncSummary = { + checkedAt: string; + totalCandidates: number; + syncedRuns: DeployRunSummary[]; + failedRuns: DeployRunJenkinsSyncError[]; +}; diff --git a/src/deploy-runs/deploy-runs.controller.ts b/src/deploy-runs/deploy-runs.controller.ts new file mode 100644 index 0000000..42ab0e9 --- /dev/null +++ b/src/deploy-runs/deploy-runs.controller.ts @@ -0,0 +1,69 @@ +import { Body, Controller, Get, Param, Post } from '@nestjs/common'; +import { + ApiBody, + ApiCreatedResponse, + ApiOkResponse, + ApiTags, +} from '@nestjs/swagger'; +import { ZodValidationPipe } from '../common/pipes/zod-validation.pipe'; +import { + CreateDeployRunDto, + createDeployRunSchema, +} from './create-deploy-run.dto'; +import { + CreateDeployRunInput, + DeployRunJenkinsSyncSummary, + DeployRunSummary, +} from './deploy-run.types'; +import { DeployRunsService } from './deploy-runs.service'; + +@ApiTags('deploy-runs') +@Controller('deploy-runs') +export class DeployRunsController { + constructor(private readonly deployRunsService: DeployRunsService) {} + + @Get() + @ApiOkResponse({ description: 'Latest deploy runs.' }) + async listRuns(): Promise { + return this.deployRunsService.listRuns(); + } + + @Get(':id') + @ApiOkResponse({ description: 'Deploy run detail.' }) + async getRun(@Param('id') id: string): Promise { + return this.deployRunsService.getRun(id); + } + + @Post() + @ApiBody({ type: CreateDeployRunDto }) + @ApiCreatedResponse({ description: 'Create a deploy run draft.' }) + async createRun( + @Body(new ZodValidationPipe(createDeployRunSchema)) body: CreateDeployRunInput, + ): Promise { + return this.deployRunsService.createRun(body); + } + + @Post(':id/cancel') + @ApiOkResponse({ description: 'Cancel a deploy run.' }) + async cancelRun(@Param('id') id: string): Promise { + return this.deployRunsService.cancelRun(id); + } + + @Post(':id/retry') + @ApiCreatedResponse({ description: 'Retry a deploy run.' }) + async retryRun(@Param('id') id: string): Promise { + return this.deployRunsService.retryRun(id); + } + + @Post('sync-jenkins') + @ApiOkResponse({ description: 'Synchronize all queued/running Jenkins runs.' }) + async syncJenkinsRuns(): Promise { + return this.deployRunsService.syncJenkinsRuns(); + } + + @Post(':id/sync-jenkins') + @ApiOkResponse({ description: 'Synchronize Jenkins queue/build state.' }) + async syncJenkinsRun(@Param('id') id: string): Promise { + return this.deployRunsService.syncJenkinsRun(id); + } +} diff --git a/src/deploy-runs/deploy-runs.module.ts b/src/deploy-runs/deploy-runs.module.ts new file mode 100644 index 0000000..0f2e665 --- /dev/null +++ b/src/deploy-runs/deploy-runs.module.ts @@ -0,0 +1,17 @@ +import { Module } from '@nestjs/common'; +import { AuditModule } from '../audit/audit.module'; +import { JenkinsModule } from '../integrations/jenkins/jenkins.module'; +import { WeComModule } from '../notifications/wecom/wecom.module'; +import { ProjectsModule } from '../projects/projects.module'; +import { DeployExecutionService } from './deploy-execution.service'; +import { DeployRunRepository } from './deploy-run.repository'; +import { DeployRunsController } from './deploy-runs.controller'; +import { DeployRunsService } from './deploy-runs.service'; + +@Module({ + imports: [AuditModule, JenkinsModule, ProjectsModule, WeComModule], + controllers: [DeployRunsController], + providers: [DeployExecutionService, DeployRunRepository, DeployRunsService], + exports: [DeployExecutionService, DeployRunRepository, DeployRunsService], +}) +export class DeployRunsModule {} diff --git a/src/deploy-runs/deploy-runs.service.spec.ts b/src/deploy-runs/deploy-runs.service.spec.ts new file mode 100644 index 0000000..610a3e4 --- /dev/null +++ b/src/deploy-runs/deploy-runs.service.spec.ts @@ -0,0 +1,250 @@ +import { AuditService } from '../audit/audit.service'; +import { AppError } from '../common/errors/app-error'; +import { DeployNotificationService } from '../notifications/wecom/deploy-notification.service'; +import { ProjectsService } from '../projects/projects.service'; +import { DeployExecutionService } from './deploy-execution.service'; +import { DeployRunRepository } from './deploy-run.repository'; +import { + CreateDeployRunInput, + DeployRunStatus, + DeployRunSummary, +} from './deploy-run.types'; +import { DeployRunsService } from './deploy-runs.service'; + +/** + * 覆盖发布单第一阶段内存实现,后续接真实数据库时保持服务契约不变。 + */ +describe('DeployRunsService', () => { + const baseRun: DeployRunSummary = { + id: 'run_001', + projectKey: 'access-manage', + environment: 'test', + ref: 'develop', + status: 'pending', + trigger: 'manual', + operator: 'ops-admin', + createdAt: '2026-06-11T00:00:00.000Z', + steps: [], + }; + + function createService(options?: { runs?: DeployRunSummary[] }) { + const runs = options?.runs ?? [baseRun]; + const repository = { + findMany: jest.fn(() => Promise.resolve(runs)), + findById: jest.fn((id: string) => + Promise.resolve(runs.find((run) => run.id === id) ?? null), + ), + create: jest.fn((input: CreateDeployRunInput) => ({ + ...baseRun, + id: input.idempotencyKey ?? 'generated_run', + projectKey: input.projectKey, + environment: input.environment, + ref: input.ref, + operator: input.operator, + trigger: input.trigger ?? 'manual', + })), + updateMemoryStatus: jest.fn((id: string, status: DeployRunStatus) => + id === baseRun.id ? { ...baseRun, status } : null, + ), + } satisfies Pick< + DeployRunRepository, + 'findMany' | 'findById' | 'create' | 'updateMemoryStatus' + >; + const projects = { + getProject: jest.fn(() => + Promise.resolve({ + id: 'project_001', + key: 'access-manage', + name: 'access-manage', + repositoryUrl: + 'https://gitea.ops.mrzhan.top/my-project/access-manage.git', + defaultBranch: 'develop', + status: 'active' as const, + environments: [ + { + name: 'test' as const, + displayName: '测试环境', + jenkinsJobPath: 'my-project-dev/test-access-manage-develop', + }, + ], + }), + ), + } satisfies Pick; + const audit = { + record: jest.fn(), + } satisfies Pick; + const notification = { + sendDeployRunEvent: jest.fn(() => + Promise.resolve({ + status: 'skipped' as const, + channel: 'wecom' as const, + reason: 'Missing WECOM_WEBHOOK_URL', + }), + ), + } satisfies Pick; + const execution = { + execute: jest.fn((run: DeployRunSummary) => + Promise.resolve({ ...run, status: 'success' as const }), + ), + syncJenkinsRun: jest.fn((run: DeployRunSummary) => + Promise.resolve({ ...run, status: 'success' as const }), + ), + } satisfies Pick; + + return { + audit, + execution, + notification, + projects, + repository, + service: new DeployRunsService( + repository as unknown as DeployRunRepository, + projects as unknown as ProjectsService, + audit as unknown as AuditService, + notification as unknown as DeployNotificationService, + execution as unknown as DeployExecutionService, + ), + }; + } + + it('creates a manual deploy run and records audit data', async () => { + const { audit, execution, repository, service } = createService(); + + const run = await service.createRun({ + projectKey: 'access-manage', + environment: 'test', + ref: 'develop', + operator: 'ops-admin', + idempotencyKey: 'idem_001', + }); + + expect(run.id).toBe('idem_001'); + expect(repository.create).toHaveBeenCalledWith( + expect.objectContaining({ projectKey: 'access-manage' }), + ); + expect(audit.record).toHaveBeenCalledWith( + expect.objectContaining({ + action: 'DEPLOY_REQUESTED', + resourceId: 'idem_001', + }), + ); + expect(execution.execute).toHaveBeenCalledWith( + expect.objectContaining({ id: 'idem_001' }), + expect.objectContaining({ key: 'access-manage' }), + ); + }); + + it('rejects unsupported environments before creating a run', async () => { + const { execution, repository, service } = createService(); + + await expect( + service.createRun({ + projectKey: 'access-manage', + environment: 'staging', + ref: 'develop', + operator: 'ops-admin', + } as unknown as CreateDeployRunInput), + ).rejects.toBeInstanceOf(AppError); + expect(repository.create).not.toHaveBeenCalled(); + expect(execution.execute).not.toHaveBeenCalled(); + }); + + it('cancels an existing deploy run', async () => { + const { audit, service } = createService(); + + const run = await service.cancelRun(baseRun.id); + + expect(run.status).toBe('canceled'); + expect(audit.record).toHaveBeenCalledWith( + expect.objectContaining({ action: 'DEPLOY_CANCELED' }), + ); + }); + + it('retries a deploy run with retry trigger', async () => { + const { repository, service } = createService(); + + const run = await service.retryRun(baseRun.id); + + expect(run.trigger).toBe('retry'); + expect(repository.create).toHaveBeenCalledWith( + expect.objectContaining({ + projectKey: baseRun.projectKey, + environment: baseRun.environment, + trigger: 'retry', + }), + ); + }); + + it('syncs all queued or running Jenkins runs with queue/build identifiers', async () => { + const queuedRun: DeployRunSummary = { + ...baseRun, + id: 'run_queued', + status: 'queued', + jenkinsQueueId: 'queue_001', + }; + const runningRun: DeployRunSummary = { + ...baseRun, + id: 'run_running', + status: 'running', + jenkinsBuildNumber: 7, + }; + const ignoredRun: DeployRunSummary = { + ...baseRun, + id: 'run_running_without_jenkins', + status: 'running', + }; + const { audit, execution, service } = createService({ + runs: [queuedRun, runningRun, ignoredRun], + }); + + const result = await service.syncJenkinsRuns(); + + expect(result.totalCandidates).toBe(2); + expect(result.syncedRuns).toHaveLength(2); + expect(result.failedRuns).toHaveLength(0); + expect(execution.syncJenkinsRun).toHaveBeenCalledTimes(2); + expect(execution.syncJenkinsRun).toHaveBeenCalledWith( + queuedRun, + expect.objectContaining({ key: queuedRun.projectKey }), + ); + expect(execution.syncJenkinsRun).toHaveBeenCalledWith( + runningRun, + expect.objectContaining({ key: runningRun.projectKey }), + ); + expect(audit.record).toHaveBeenCalledWith( + expect.objectContaining({ + action: 'JENKINS_SYNC_SWEEP_COMPLETED', + after: expect.objectContaining({ + totalCandidates: 2, + syncedCount: 2, + failedCount: 0, + }), + }), + ); + }); + + it('returns a redacted error when one Jenkins run fails during bulk sync', async () => { + const queuedRun: DeployRunSummary = { + ...baseRun, + id: 'run_queued', + status: 'queued', + jenkinsQueueId: 'queue_001', + }; + const { execution, service } = createService({ runs: [queuedRun] }); + execution.syncJenkinsRun.mockRejectedValueOnce( + new Error('abcdefghijklmnopqrstuvwxyz123456'), + ); + + const result = await service.syncJenkinsRuns(); + + expect(result.totalCandidates).toBe(1); + expect(result.syncedRuns).toHaveLength(0); + expect(result.failedRuns).toEqual([ + { + id: 'run_queued', + projectKey: 'access-manage', + message: '[REDACTED]', + }, + ]); + }); +}); diff --git a/src/deploy-runs/deploy-runs.service.ts b/src/deploy-runs/deploy-runs.service.ts new file mode 100644 index 0000000..968ded4 --- /dev/null +++ b/src/deploy-runs/deploy-runs.service.ts @@ -0,0 +1,198 @@ +import { Injectable } from '@nestjs/common'; +import { AppError } from '../common/errors/app-error'; +import { redactSensitive } from '../common/security/redact-sensitive'; +import { AuditService } from '../audit/audit.service'; +import { + DeployNotificationEvent, + DeployNotificationService, +} from '../notifications/wecom/deploy-notification.service'; +import { ProjectsService } from '../projects/projects.service'; +import { DeployExecutionService } from './deploy-execution.service'; +import { DeployRunRepository } from './deploy-run.repository'; +import { + CreateDeployRunInput, + DeployRunJenkinsSyncSummary, + DeployRunSummary, +} from './deploy-run.types'; + +@Injectable() +export class DeployRunsService { + constructor( + private readonly deployRunRepository: DeployRunRepository, + private readonly projectsService: ProjectsService, + private readonly auditService: AuditService, + private readonly deployNotificationService: DeployNotificationService, + private readonly deployExecutionService: DeployExecutionService, + ) {} + + async listRuns(): Promise { + return this.deployRunRepository.findMany(); + } + + async getRun(id: string): Promise { + const run = await this.deployRunRepository.findById(id); + + if (!run) { + throw new AppError( + 'RESOURCE_NOT_FOUND', + `Deploy run ${id} not found`, + 404, + { + id, + }, + ); + } + + return run; + } + + async createRun(input: CreateDeployRunInput): Promise { + if (!['test', 'production'].includes(input.environment)) { + throw new AppError( + 'VALIDATION_FAILED', + 'environment must be test or production', + 400, + { environment: input.environment }, + ); + } + + const project = await this.projectsService.getProject(input.projectKey); + const run = this.deployRunRepository.create(input); + + this.auditService.record({ + action: 'DEPLOY_REQUESTED', + resourceType: 'deploy_run', + resourceId: run.id, + actorName: input.operator, + after: { + projectKey: input.projectKey, + environment: input.environment, + ref: input.ref, + remark: input.remark, + }, + }); + + return this.deployExecutionService.execute(run, project); + } + + async cancelRun(id: string): Promise { + const run = this.deployRunRepository.updateMemoryStatus(id, 'canceled'); + + if (!run) { + throw new AppError( + 'RESOURCE_NOT_FOUND', + `Deploy run ${id} not found`, + 404, + { + id, + }, + ); + } + + this.auditService.record({ + action: 'DEPLOY_CANCELED', + resourceType: 'deploy_run', + resourceId: run.id, + after: { + status: run.status, + }, + }); + await this.recordNotificationResult(run, 'canceled'); + + return run; + } + + async retryRun(id: string): Promise { + const source = await this.getRun(id); + return this.createRun({ + projectKey: source.projectKey, + environment: source.environment, + ref: source.ref, + operator: source.operator, + remark: `Retry of ${source.id}`, + idempotencyKey: `${source.id}:retry:${new Date().toISOString()}`, + trigger: 'retry', + }); + } + + async syncJenkinsRun(id: string): Promise { + const run = await this.getRun(id); + const project = await this.projectsService.getProject(run.projectKey); + + return this.deployExecutionService.syncJenkinsRun(run, project); + } + + async syncJenkinsRuns(): Promise { + const runs = await this.listRuns(); + const candidates = runs.filter((run) => this.isJenkinsSyncCandidate(run)); + const syncedRuns: DeployRunSummary[] = []; + const failedRuns: DeployRunJenkinsSyncSummary['failedRuns'] = []; + + for (const run of candidates) { + try { + const project = await this.projectsService.getProject(run.projectKey); + const syncedRun = await this.deployExecutionService.syncJenkinsRun( + run, + project, + ); + syncedRuns.push(syncedRun); + } catch (error) { + failedRuns.push({ + id: run.id, + projectKey: run.projectKey, + message: this.toSafeSyncError(error), + }); + } + } + + this.auditService.record({ + action: 'JENKINS_SYNC_SWEEP_COMPLETED', + resourceType: 'deploy_run', + after: { + totalCandidates: candidates.length, + syncedCount: syncedRuns.length, + failedCount: failedRuns.length, + }, + }); + + return { + checkedAt: new Date().toISOString(), + totalCandidates: candidates.length, + syncedRuns, + failedRuns, + }; + } + + private async recordNotificationResult( + run: DeployRunSummary, + event: DeployNotificationEvent, + ): Promise { + const notification = + await this.deployNotificationService.sendDeployRunEvent(run, event); + + this.auditService.record({ + action: `WECOM_NOTIFICATION_${notification.status.toUpperCase()}`, + resourceType: 'deploy_run', + resourceId: run.id, + after: { + event, + channel: notification.channel, + reason: notification.reason, + }, + }); + } + + private isJenkinsSyncCandidate(run: DeployRunSummary): boolean { + return ( + ['queued', 'running'].includes(run.status) && + Boolean(run.jenkinsQueueId || run.jenkinsBuildNumber) + ); + } + + private toSafeSyncError(error: unknown): string { + const message = + error instanceof Error ? error.message : 'Jenkins sync failed'; + + return redactSensitive(message); + } +} diff --git a/src/health/health.controller.ts b/src/health/health.controller.ts new file mode 100644 index 0000000..2f041cc --- /dev/null +++ b/src/health/health.controller.ts @@ -0,0 +1,15 @@ +import { Controller, Get } from '@nestjs/common'; +import { ApiOkResponse, ApiTags } from '@nestjs/swagger'; +import { HealthService } from './health.service'; + +@ApiTags('health') +@Controller('health') +export class HealthController { + constructor(private readonly healthService: HealthService) {} + + @Get() + @ApiOkResponse({ description: 'Runtime dependency health summary.' }) + async getHealth(): Promise> { + return this.healthService.getHealth(); + } +} diff --git a/src/health/health.module.ts b/src/health/health.module.ts new file mode 100644 index 0000000..f605e02 --- /dev/null +++ b/src/health/health.module.ts @@ -0,0 +1,14 @@ +import { Module } from '@nestjs/common'; +import { AgentModule } from '../agent/agent.module'; +import { GiteaModule } from '../integrations/gitea/gitea.module'; +import { JenkinsModule } from '../integrations/jenkins/jenkins.module'; +import { WeComModule } from '../notifications/wecom/wecom.module'; +import { HealthController } from './health.controller'; +import { HealthService } from './health.service'; + +@Module({ + imports: [AgentModule, GiteaModule, JenkinsModule, WeComModule], + controllers: [HealthController], + providers: [HealthService], +}) +export class HealthModule {} diff --git a/src/health/health.service.spec.ts b/src/health/health.service.spec.ts new file mode 100644 index 0000000..391b8ab --- /dev/null +++ b/src/health/health.service.spec.ts @@ -0,0 +1,50 @@ +import { ConfigService } from '@nestjs/config'; +import { LlmClient } from '../agent/llm.client'; +import { EnvConfig } from '../config/env.schema'; +import { GiteaClient } from '../integrations/gitea/gitea.client'; +import { JenkinsClient } from '../integrations/jenkins/jenkins.client'; +import { WeComClient } from '../notifications/wecom/wecom.client'; +import { PrismaService } from '../prisma/prisma.service'; +import { HealthService } from './health.service'; + +/** + * 健康检查是前端总览和后续部署前置校验的统一数据入口。 + */ +describe('HealthService', () => { + it('returns ok when optional integrations are not configured but available services respond', async () => { + const config = { + get: jest.fn((key: keyof EnvConfig) => (key === 'REDIS_URL' ? undefined : undefined)), + } satisfies Pick, 'get'>; + const prisma = { + databaseHealth: jest.fn(() => + Promise.resolve({ status: 'not_configured' as const }), + ), + } satisfies Pick; + const jenkins = { + healthSummary: jest.fn(() => ({ status: 'not_configured' as const })), + } satisfies Pick; + const gitea = { + healthSummary: jest.fn(() => ({ status: 'not_configured' as const })), + } satisfies Pick; + const weCom = { + healthSummary: jest.fn(() => ({ status: 'not_configured' as const })), + } satisfies Pick; + const llm = { + healthSummary: jest.fn(() => ({ status: 'not_configured' as const })), + } satisfies Pick; + const service = new HealthService( + config as unknown as ConfigService, + prisma as unknown as PrismaService, + jenkins as unknown as JenkinsClient, + gitea as unknown as GiteaClient, + weCom as unknown as WeComClient, + llm as unknown as LlmClient, + ); + + const health = await service.getHealth(); + + expect(health.status).toBe('ok'); + expect(health.dependencies.database.status).toBe('not_configured'); + expect(health.dependencies.jenkins.status).toBe('not_configured'); + }); +}); diff --git a/src/health/health.service.ts b/src/health/health.service.ts new file mode 100644 index 0000000..2d71af4 --- /dev/null +++ b/src/health/health.service.ts @@ -0,0 +1,48 @@ +import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { LlmClient } from '../agent/llm.client'; +import { EnvConfig } from '../config/env.schema'; +import { GiteaClient } from '../integrations/gitea/gitea.client'; +import { JenkinsClient } from '../integrations/jenkins/jenkins.client'; +import { WeComClient } from '../notifications/wecom/wecom.client'; +import { PrismaService } from '../prisma/prisma.service'; + +type HealthStatus = 'ok' | 'not_configured' | 'unavailable'; + +@Injectable() +export class HealthService { + constructor( + private readonly config: ConfigService, + private readonly prisma: PrismaService, + private readonly jenkins: JenkinsClient, + private readonly gitea: GiteaClient, + private readonly weCom: WeComClient, + private readonly llm: LlmClient, + ) {} + + async getHealth(): Promise<{ + status: HealthStatus; + checkedAt: string; + dependencies: Record; + }> { + const dependencies = { + database: await this.prisma.databaseHealth(), + redis: this.config.get('REDIS_URL', { infer: true }) + ? { status: 'ok' as const } + : { status: 'not_configured' as const }, + jenkins: this.jenkins.healthSummary(), + gitea: this.gitea.healthSummary(), + wecom: this.weCom.healthSummary(), + llm: this.llm.healthSummary(), + }; + const hasUnavailable = Object.values(dependencies).some( + (dependency) => dependency.status === 'unavailable', + ); + + return { + status: hasUnavailable ? 'unavailable' : 'ok', + checkedAt: new Date().toISOString(), + dependencies, + }; + } +} diff --git a/src/integrations/gitea/gitea.client.ts b/src/integrations/gitea/gitea.client.ts new file mode 100644 index 0000000..b40308f --- /dev/null +++ b/src/integrations/gitea/gitea.client.ts @@ -0,0 +1,105 @@ +import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { IntegrationRequestError, IntegrationUnavailableError } from '../../common/errors/integration-error'; +import { redactSensitive } from '../../common/security/redact-sensitive'; +import { EnvConfig } from '../../config/env.schema'; +import { GiteaBranch, GiteaCommitSummary, GiteaTag } from './gitea.types'; + +@Injectable() +export class GiteaClient { + constructor(private readonly config: ConfigService) {} + + healthSummary(): { status: 'ok' | 'not_configured'; message?: string } { + const missing = this.missingConfig(); + return missing.length === 0 + ? { status: 'ok' } + : { status: 'not_configured', message: `Missing ${missing.join(', ')}` }; + } + + async listBranches(owner: string, repo: string): Promise { + const payload = await this.request>( + `/api/v1/repos/${owner}/${repo}/branches`, + ); + + return payload.map((branch) => ({ + name: branch.name, + commitSha: branch.commit.id, + })); + } + + async listTags(owner: string, repo: string): Promise { + const payload = await this.request>( + `/api/v1/repos/${owner}/${repo}/tags`, + ); + + return payload.map((tag) => ({ + name: tag.name, + commitSha: tag.commit.sha, + })); + } + + async getCommit( + owner: string, + repo: string, + ref: string, + ): Promise { + const payload = await this.request<{ + sha: string; + commit: { + message: string; + author?: { + name?: string; + date?: string; + }; + }; + }>(`/api/v1/repos/${owner}/${repo}/git/commits/${encodeURIComponent(ref)}`); + + return { + sha: payload.sha, + message: payload.commit.message, + authorName: payload.commit.author?.name, + authoredAt: payload.commit.author?.date, + }; + } + + private async request(path: string, init: RequestInit = {}): Promise { + this.assertConfigured(); + const baseUrl = this.config.get('GITEA_BASE_URL', { infer: true }) ?? ''; + const token = this.config.get('GITEA_TOKEN', { infer: true }) ?? ''; + const response = await fetch(`${baseUrl}${path}`, { + ...init, + headers: { + authorization: `token ${token}`, + accept: 'application/json', + ...(init.headers ?? {}), + }, + }); + + if (!response.ok) { + throw new IntegrationRequestError('gitea', 'Gitea request failed', { + status: response.status, + path, + request: redactSensitive(init), + }); + } + + return (await response.json()) as T; + } + + private assertConfigured(): void { + const missing = this.missingConfig(); + + if (missing.length > 0) { + throw new IntegrationUnavailableError('gitea', missing); + } + } + + private missingConfig(): string[] { + const entries: Array<[string, string | undefined]> = [ + ['GITEA_BASE_URL', this.config.get('GITEA_BASE_URL', { infer: true })], + ['GITEA_TOKEN', this.config.get('GITEA_TOKEN', { infer: true })], + ]; + + return entries.filter(([, value]) => !value).map(([key]) => key); + } +} diff --git a/src/integrations/gitea/gitea.module.ts b/src/integrations/gitea/gitea.module.ts new file mode 100644 index 0000000..10470be --- /dev/null +++ b/src/integrations/gitea/gitea.module.ts @@ -0,0 +1,8 @@ +import { Module } from '@nestjs/common'; +import { GiteaClient } from './gitea.client'; + +@Module({ + providers: [GiteaClient], + exports: [GiteaClient], +}) +export class GiteaModule {} diff --git a/src/integrations/gitea/gitea.types.ts b/src/integrations/gitea/gitea.types.ts new file mode 100644 index 0000000..859dac8 --- /dev/null +++ b/src/integrations/gitea/gitea.types.ts @@ -0,0 +1,16 @@ +export type GiteaBranch = { + name: string; + commitSha: string; +}; + +export type GiteaTag = { + name: string; + commitSha: string; +}; + +export type GiteaCommitSummary = { + sha: string; + message: string; + authorName?: string; + authoredAt?: string; +}; diff --git a/src/integrations/jenkins/jenkins.client.ts b/src/integrations/jenkins/jenkins.client.ts new file mode 100644 index 0000000..30b851f --- /dev/null +++ b/src/integrations/jenkins/jenkins.client.ts @@ -0,0 +1,139 @@ +import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { IntegrationRequestError, IntegrationUnavailableError } from '../../common/errors/integration-error'; +import { redactSensitive } from '../../common/security/redact-sensitive'; +import { EnvConfig } from '../../config/env.schema'; +import { + JenkinsBuildStatus, + JenkinsProgressiveText, + JenkinsQueueItem, +} from './jenkins.types'; + +@Injectable() +export class JenkinsClient { + constructor(private readonly config: ConfigService) {} + + healthSummary(): { status: 'ok' | 'not_configured'; message?: string } { + const missing = this.missingConfig(); + return missing.length === 0 + ? { status: 'ok' } + : { status: 'not_configured', message: `Missing ${missing.join(', ')}` }; + } + + async buildWithParameters( + jobPath: string, + parameters: Record, + ): Promise { + this.assertConfigured(); + const body = new URLSearchParams(parameters); + const response = await this.request( + `/job/${jobPath.split('/').map(encodeURIComponent).join('/job/')}/buildWithParameters`, + { + method: 'POST', + body, + }, + ); + + const location = response.headers.get('location') ?? undefined; + return { + queueId: location?.split('/').filter(Boolean).pop() ?? 'unknown', + executableUrl: location, + }; + } + + async getQueueItem(queueId: string): Promise { + this.assertConfigured(); + const response = await this.request(`/queue/item/${queueId}/api/json`); + const payload = (await response.json()) as { + id?: number; + executable?: { number?: number; url?: string }; + }; + + return { + queueId: String(payload.id ?? queueId), + buildNumber: payload.executable?.number, + executableUrl: payload.executable?.url, + }; + } + + async getBuildStatus( + jobPath: string, + buildNumber: number, + ): Promise { + this.assertConfigured(); + const response = await this.request( + `/job/${jobPath.split('/').map(encodeURIComponent).join('/job/')}/${buildNumber}/api/json`, + ); + const payload = (await response.json()) as { + building?: boolean; + result?: JenkinsBuildStatus['result']; + url?: string; + }; + + return { + jobPath, + buildNumber, + building: Boolean(payload.building), + result: payload.result, + url: payload.url, + }; + } + + async getProgressiveText( + jobPath: string, + buildNumber: number, + start = 0, + ): Promise { + this.assertConfigured(); + const response = await this.request( + `/job/${jobPath.split('/').map(encodeURIComponent).join('/job/')}/${buildNumber}/logText/progressiveText?start=${start}`, + ); + + return { + text: await response.text(), + nextStart: Number(response.headers.get('x-text-size') ?? start), + hasMore: response.headers.get('x-more-data') === 'true', + }; + } + + private async request(path: string, init: RequestInit = {}): Promise { + const baseUrl = this.config.get('JENKINS_BASE_URL', { infer: true }) ?? ''; + const username = this.config.get('JENKINS_USERNAME', { infer: true }) ?? ''; + const apiToken = this.config.get('JENKINS_API_TOKEN', { infer: true }) ?? ''; + const response = await fetch(`${baseUrl}${path}`, { + ...init, + headers: { + authorization: `Basic ${Buffer.from(`${username}:${apiToken}`).toString('base64')}`, + ...(init.headers ?? {}), + }, + }); + + if (!response.ok) { + throw new IntegrationRequestError('jenkins', 'Jenkins request failed', { + status: response.status, + path, + request: redactSensitive(init), + }); + } + + return response; + } + + private assertConfigured(): void { + const missing = this.missingConfig(); + + if (missing.length > 0) { + throw new IntegrationUnavailableError('jenkins', missing); + } + } + + private missingConfig(): string[] { + const entries: Array<[string, string | undefined]> = [ + ['JENKINS_BASE_URL', this.config.get('JENKINS_BASE_URL', { infer: true })], + ['JENKINS_USERNAME', this.config.get('JENKINS_USERNAME', { infer: true })], + ['JENKINS_API_TOKEN', this.config.get('JENKINS_API_TOKEN', { infer: true })], + ]; + + return entries.filter(([, value]) => !value).map(([key]) => key); + } +} diff --git a/src/integrations/jenkins/jenkins.module.ts b/src/integrations/jenkins/jenkins.module.ts new file mode 100644 index 0000000..5638924 --- /dev/null +++ b/src/integrations/jenkins/jenkins.module.ts @@ -0,0 +1,8 @@ +import { Module } from '@nestjs/common'; +import { JenkinsClient } from './jenkins.client'; + +@Module({ + providers: [JenkinsClient], + exports: [JenkinsClient], +}) +export class JenkinsModule {} diff --git a/src/integrations/jenkins/jenkins.types.ts b/src/integrations/jenkins/jenkins.types.ts new file mode 100644 index 0000000..e65fed0 --- /dev/null +++ b/src/integrations/jenkins/jenkins.types.ts @@ -0,0 +1,19 @@ +export type JenkinsQueueItem = { + queueId: string; + buildNumber?: number; + executableUrl?: string; +}; + +export type JenkinsBuildStatus = { + jobPath: string; + buildNumber: number; + building: boolean; + result?: 'SUCCESS' | 'FAILURE' | 'ABORTED' | 'UNSTABLE'; + url?: string; +}; + +export type JenkinsProgressiveText = { + text: string; + nextStart: number; + hasMore: boolean; +}; diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..e178aa7 --- /dev/null +++ b/src/main.ts @@ -0,0 +1,35 @@ +import { NestFactory } from '@nestjs/core'; +import { ConfigService } from '@nestjs/config'; +import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; +import { AppModule } from './app.module'; +import { ApiEnvelopeInterceptor } from './common/http/api-envelope.interceptor'; +import { AllExceptionsFilter } from './common/http/all-exceptions.filter'; +import { EnvConfig } from './config/env.schema'; + +async function bootstrap(): Promise { + const app = await NestFactory.create(AppModule, { + bufferLogs: true, + rawBody: true, + }); + const config = app.get(ConfigService); + const corsOrigin = config.get('CORS_ORIGIN', { infer: true }); + + app.enableCors({ + origin: corsOrigin ? corsOrigin.split(',').map((origin) => origin.trim()) : true, + credentials: true, + }); + app.useGlobalFilters(new AllExceptionsFilter()); + app.useGlobalInterceptors(new ApiEnvelopeInterceptor()); + + const swaggerConfig = new DocumentBuilder() + .setTitle('DevOps Platform API') + .setDescription('Jenkins, Gitea, notification, BPMN deploy runs, and agent operation APIs.') + .setVersion('0.1.0') + .build(); + const document = SwaggerModule.createDocument(app, swaggerConfig); + SwaggerModule.setup('docs', app, document); + + await app.listen(config.get('PORT', { infer: true })); +} + +void bootstrap(); diff --git a/src/notifications/wecom/deploy-notification.service.spec.ts b/src/notifications/wecom/deploy-notification.service.spec.ts new file mode 100644 index 0000000..c11d4a1 --- /dev/null +++ b/src/notifications/wecom/deploy-notification.service.spec.ts @@ -0,0 +1,58 @@ +import { WeComClient } from './wecom.client'; +import { DeployNotificationService } from './deploy-notification.service'; +import { DeployRunSummary } from '../../deploy-runs/deploy-run.types'; + +/** + * 覆盖企微通知的安全边界:缺少 webhook 时跳过,配置后才发送。 + */ +describe('DeployNotificationService', () => { + const run: DeployRunSummary = { + id: 'run_001', + projectKey: 'access-manage', + environment: 'test', + ref: 'develop', + status: 'pending', + trigger: 'manual', + operator: 'ops-admin', + createdAt: '2026-06-11T00:00:00.000Z', + steps: [], + }; + + it('skips notification when WeCom is not configured', async () => { + const weCom = { + healthSummary: jest.fn(() => ({ + status: 'not_configured' as const, + message: 'Missing WECOM_WEBHOOK_URL', + })), + sendMarkdown: jest.fn(), + } satisfies Pick; + const service = new DeployNotificationService( + weCom as unknown as WeComClient, + ); + + const result = await service.sendDeployRunEvent(run, 'created'); + + expect(result.status).toBe('skipped'); + expect(weCom.sendMarkdown).not.toHaveBeenCalled(); + }); + + it('sends markdown when WeCom is configured', async () => { + const weCom = { + healthSummary: jest.fn(() => ({ status: 'ok' as const })), + sendMarkdown: jest.fn(() => Promise.resolve({ accepted: true })), + } satisfies Pick; + const service = new DeployNotificationService( + weCom as unknown as WeComClient, + ); + + const result = await service.sendDeployRunEvent(run, 'created'); + + expect(result.status).toBe('sent'); + expect(weCom.sendMarkdown).toHaveBeenCalledWith( + expect.objectContaining({ + title: 'DevOps 发布已创建', + markdown: expect.stringContaining('access-manage'), + }), + ); + }); +}); diff --git a/src/notifications/wecom/deploy-notification.service.ts b/src/notifications/wecom/deploy-notification.service.ts new file mode 100644 index 0000000..8f2202e --- /dev/null +++ b/src/notifications/wecom/deploy-notification.service.ts @@ -0,0 +1,103 @@ +import { Injectable } from '@nestjs/common'; +import { redactSensitive } from '../../common/security/redact-sensitive'; +import { DeployRunSummary } from '../../deploy-runs/deploy-run.types'; +import { WeComClient } from './wecom.client'; + +export type DeployNotificationEvent = + | 'created' + | 'queued' + | 'succeeded' + | 'failed' + | 'canceled' + | 'retried'; + +export type DeployNotificationResult = { + status: 'sent' | 'skipped' | 'failed'; + channel: 'wecom'; + reason?: string; +}; + +/** + * 统一封装发布单企微通知,保证 webhook 只存在于后端配置边界内。 + */ +@Injectable() +export class DeployNotificationService { + constructor(private readonly weComClient: WeComClient) {} + + async sendDeployRunEvent( + run: DeployRunSummary, + event: DeployNotificationEvent, + ): Promise { + const health = this.weComClient.healthSummary(); + + if (health.status === 'not_configured') { + return { + status: 'skipped', + channel: 'wecom', + reason: health.message ?? 'WeCom webhook is not configured', + }; + } + + try { + await this.weComClient.sendMarkdown({ + title: this.titleFor(event), + markdown: this.markdownFor(run, event), + }); + + return { status: 'sent', channel: 'wecom' }; + } catch (error) { + return { + status: 'failed', + channel: 'wecom', + reason: + error instanceof Error + ? redactSensitive({ message: error.message }).message + : 'Unknown WeCom notification error', + }; + } + } + + private titleFor(event: DeployNotificationEvent): string { + if (event === 'canceled') { + return 'DevOps 发布已取消'; + } + + if (event === 'queued') { + return 'DevOps 发布已进入 Jenkins 队列'; + } + + if (event === 'succeeded') { + return 'DevOps 发布成功'; + } + + if (event === 'failed') { + return 'DevOps 发布失败'; + } + + if (event === 'retried') { + return 'DevOps 发布已重试'; + } + + return 'DevOps 发布已创建'; + } + + private markdownFor( + run: DeployRunSummary, + event: DeployNotificationEvent, + ): string { + return [ + `> 事件:${this.titleFor(event)}`, + `> 项目:${run.projectKey}`, + `> 环境:${run.environment}`, + `> Ref:${run.ref}`, + `> 状态:${run.status}`, + `> 操作者:${run.operator}`, + `> Run ID:${run.id}`, + run.jenkinsQueueId ? `> Jenkins Queue:${run.jenkinsQueueId}` : undefined, + run.jenkinsBuildNumber ? `> Jenkins Build:#${run.jenkinsBuildNumber}` : undefined, + run.failureSummary ? `> 失败摘要:${run.failureSummary}` : undefined, + ] + .filter(Boolean) + .join('\n'); + } +} diff --git a/src/notifications/wecom/wecom.client.ts b/src/notifications/wecom/wecom.client.ts new file mode 100644 index 0000000..844deaa --- /dev/null +++ b/src/notifications/wecom/wecom.client.ts @@ -0,0 +1,49 @@ +import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { IntegrationRequestError, IntegrationUnavailableError } from '../../common/errors/integration-error'; +import { redactSensitive } from '../../common/security/redact-sensitive'; +import { EnvConfig } from '../../config/env.schema'; +import { WeComMarkdownMessage, WeComSendResult } from './wecom.types'; + +@Injectable() +export class WeComClient { + constructor(private readonly config: ConfigService) {} + + healthSummary(): { status: 'ok' | 'not_configured'; message?: string } { + return this.config.get('WECOM_WEBHOOK_URL', { infer: true }) + ? { status: 'ok' } + : { status: 'not_configured', message: 'Missing WECOM_WEBHOOK_URL' }; + } + + async sendMarkdown(message: WeComMarkdownMessage): Promise { + this.assertConfigured(); + const webhookUrl = this.config.get('WECOM_WEBHOOK_URL', { infer: true }) ?? ''; + const response = await fetch(webhookUrl, { + method: 'POST', + headers: { + 'content-type': 'application/json', + }, + body: JSON.stringify({ + msgtype: 'markdown', + markdown: { + content: `# ${message.title}\n\n${message.markdown}`, + }, + }), + }); + + if (!response.ok) { + throw new IntegrationRequestError('wecom', 'WeCom webhook request failed', { + status: response.status, + message: redactSensitive(message), + }); + } + + return { accepted: true }; + } + + private assertConfigured(): void { + if (!this.config.get('WECOM_WEBHOOK_URL', { infer: true })) { + throw new IntegrationUnavailableError('wecom', ['WECOM_WEBHOOK_URL']); + } + } +} diff --git a/src/notifications/wecom/wecom.module.ts b/src/notifications/wecom/wecom.module.ts new file mode 100644 index 0000000..e3a4f89 --- /dev/null +++ b/src/notifications/wecom/wecom.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; +import { DeployNotificationService } from './deploy-notification.service'; +import { WeComClient } from './wecom.client'; + +@Module({ + providers: [WeComClient, DeployNotificationService], + exports: [WeComClient, DeployNotificationService], +}) +export class WeComModule {} diff --git a/src/notifications/wecom/wecom.types.ts b/src/notifications/wecom/wecom.types.ts new file mode 100644 index 0000000..cc51a45 --- /dev/null +++ b/src/notifications/wecom/wecom.types.ts @@ -0,0 +1,9 @@ +export type WeComMarkdownMessage = { + title: string; + markdown: string; +}; + +export type WeComSendResult = { + accepted: boolean; + message?: string; +}; diff --git a/src/prisma/prisma.module.ts b/src/prisma/prisma.module.ts new file mode 100644 index 0000000..7207426 --- /dev/null +++ b/src/prisma/prisma.module.ts @@ -0,0 +1,9 @@ +import { Global, Module } from '@nestjs/common'; +import { PrismaService } from './prisma.service'; + +@Global() +@Module({ + providers: [PrismaService], + exports: [PrismaService], +}) +export class PrismaModule {} diff --git a/src/prisma/prisma.service.ts b/src/prisma/prisma.service.ts new file mode 100644 index 0000000..e9d4188 --- /dev/null +++ b/src/prisma/prisma.service.ts @@ -0,0 +1,51 @@ +import { Injectable, OnModuleDestroy, OnModuleInit } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { PrismaClient } from '@prisma/client'; +import { EnvConfig } from '../config/env.schema'; + +@Injectable() +export class PrismaService + extends PrismaClient + implements OnModuleInit, OnModuleDestroy +{ + constructor(private readonly config: ConfigService) { + super({ + log: + config.get('NODE_ENV', { infer: true }) === 'development' + ? ['warn', 'error'] + : ['error'], + }); + } + + async onModuleInit(): Promise { + if ( + this.config.get('DATABASE_URL', { infer: true }) && + this.config.get('PRISMA_CONNECT_ON_BOOT', { infer: true }) + ) { + await this.$connect(); + } + } + + async onModuleDestroy(): Promise { + await this.$disconnect(); + } + + async databaseHealth(): Promise<{ + status: 'not_configured' | 'ok' | 'unavailable'; + message?: string; + }> { + if (!this.config.get('DATABASE_URL', { infer: true })) { + return { status: 'not_configured' }; + } + + try { + await this.$connect(); + return { status: 'ok' }; + } catch (error) { + return { + status: 'unavailable', + message: error instanceof Error ? error.message : 'Unknown database error', + }; + } + } +} diff --git a/src/process-definitions/process-definition.types.ts b/src/process-definitions/process-definition.types.ts new file mode 100644 index 0000000..923f6a9 --- /dev/null +++ b/src/process-definitions/process-definition.types.ts @@ -0,0 +1,9 @@ +/** + * 前端 BPMN viewer 消费的后端流程定义契约。 + */ +export type ProcessDefinitionSummary = { + key: 'release'; + version: string; + xml: string; + nodeIds: string[]; +}; diff --git a/src/process-definitions/process-definitions.controller.ts b/src/process-definitions/process-definitions.controller.ts new file mode 100644 index 0000000..11c9a67 --- /dev/null +++ b/src/process-definitions/process-definitions.controller.ts @@ -0,0 +1,18 @@ +import { Controller, Get } from '@nestjs/common'; +import { ApiOkResponse, ApiTags } from '@nestjs/swagger'; +import { ProcessDefinitionSummary } from './process-definition.types'; +import { ProcessDefinitionsService } from './process-definitions.service'; + +@ApiTags('process-definitions') +@Controller('process-definitions') +export class ProcessDefinitionsController { + constructor( + private readonly processDefinitionsService: ProcessDefinitionsService, + ) {} + + @Get('release') + @ApiOkResponse({ description: 'BPMN process definition for deploy runs.' }) + getReleaseProcess(): ProcessDefinitionSummary { + return this.processDefinitionsService.getReleaseProcess(); + } +} diff --git a/src/process-definitions/process-definitions.module.ts b/src/process-definitions/process-definitions.module.ts new file mode 100644 index 0000000..034f62e --- /dev/null +++ b/src/process-definitions/process-definitions.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { ProcessDefinitionsController } from './process-definitions.controller'; +import { ProcessDefinitionsService } from './process-definitions.service'; + +@Module({ + controllers: [ProcessDefinitionsController], + providers: [ProcessDefinitionsService], + exports: [ProcessDefinitionsService], +}) +export class ProcessDefinitionsModule {} diff --git a/src/process-definitions/process-definitions.service.spec.ts b/src/process-definitions/process-definitions.service.spec.ts new file mode 100644 index 0000000..84a7a2c --- /dev/null +++ b/src/process-definitions/process-definitions.service.spec.ts @@ -0,0 +1,22 @@ +import { ProcessDefinitionsService } from './process-definitions.service'; + +/** + * 确保后端暴露的 BPMN 节点与 deploy run step 映射保持一致。 + */ +describe('ProcessDefinitionsService', () => { + it('returns the release BPMN definition and canonical node ids', () => { + const service = new ProcessDefinitionsService(); + const definition = service.getReleaseProcess(); + + expect(definition.key).toBe('release'); + expect(definition.xml).toContain('bpmn:definitions'); + expect(definition.nodeIds).toEqual([ + 'start-release', + 'gitea-check', + 'jenkins-build', + 'health-check', + 'notify-wecom', + 'finish-release', + ]); + }); +}); diff --git a/src/process-definitions/process-definitions.service.ts b/src/process-definitions/process-definitions.service.ts new file mode 100644 index 0000000..8ca12a5 --- /dev/null +++ b/src/process-definitions/process-definitions.service.ts @@ -0,0 +1,21 @@ +import { Injectable } from '@nestjs/common'; +import { ProcessDefinitionSummary } from './process-definition.types'; +import { + releaseProcessBpmnXml, + releaseProcessNodeIds, +} from './release-process.bpmn'; + +/** + * 管理平台内置流程定义,后续负责从数据库/版本仓库加载 BPMN。 + */ +@Injectable() +export class ProcessDefinitionsService { + getReleaseProcess(): ProcessDefinitionSummary { + return { + key: 'release', + version: 'release-v1', + xml: releaseProcessBpmnXml, + nodeIds: [...releaseProcessNodeIds], + }; + } +} diff --git a/src/process-definitions/release-process.bpmn.ts b/src/process-definitions/release-process.bpmn.ts new file mode 100644 index 0000000..04b83df --- /dev/null +++ b/src/process-definitions/release-process.bpmn.ts @@ -0,0 +1,86 @@ +/** + * 发布流程第一版 BPMN XML;后续可迁移到数据库并按环境/版本读取。 + */ +export const releaseProcessBpmnXml = ` + + + + Flow_Start_Gitea + + + Flow_Start_Gitea + Flow_Gitea_Jenkins + + + Flow_Gitea_Jenkins + Flow_Jenkins_Health + + + Flow_Jenkins_Health + Flow_Health_Notify + + + Flow_Health_Notify + Flow_Notify_End + + + Flow_Notify_End + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +`; + +export const releaseProcessNodeIds = [ + 'start-release', + 'gitea-check', + 'jenkins-build', + 'health-check', + 'notify-wecom', + 'finish-release', +] as const; diff --git a/src/projects/project.repository.ts b/src/projects/project.repository.ts new file mode 100644 index 0000000..5f98049 --- /dev/null +++ b/src/projects/project.repository.ts @@ -0,0 +1,60 @@ +import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { ProjectStatus } from '@prisma/client'; +import { EnvConfig } from '../config/env.schema'; +import { PrismaService } from '../prisma/prisma.service'; +import { seedProjects } from './project.seed'; +import { ProjectSummary } from './project.types'; + +@Injectable() +export class ProjectRepository { + constructor( + private readonly config: ConfigService, + private readonly prisma: PrismaService, + ) {} + + async findMany(): Promise { + if (!this.useDatabaseReads()) { + return seedProjects; + } + + const projects = await this.prisma.project.findMany({ + include: { + environments: { + include: { + deployJobs: true, + }, + }, + }, + orderBy: { + createdAt: 'asc', + }, + }); + + return projects.map((project) => ({ + id: project.id, + key: project.key, + name: project.name, + repositoryUrl: project.repositoryUrl ?? '', + defaultBranch: project.defaultBranch, + status: project.status === ProjectStatus.ACTIVE ? 'active' : 'archived', + environments: project.environments.map((environment) => ({ + name: environment.name === 'TEST' ? 'test' : 'production', + displayName: environment.displayName, + jenkinsJobPath: environment.deployJobs[0]?.jenkinsJobPath ?? '', + })), + })); + } + + async findByKey(key: string): Promise { + const projects = await this.findMany(); + return projects.find((project) => project.key === key) ?? null; + } + + private useDatabaseReads(): boolean { + return ( + this.config.get('USE_DATABASE_READS', { infer: true }) && + Boolean(this.config.get('DATABASE_URL', { infer: true })) + ); + } +} diff --git a/src/projects/project.seed.ts b/src/projects/project.seed.ts new file mode 100644 index 0000000..d8e2f31 --- /dev/null +++ b/src/projects/project.seed.ts @@ -0,0 +1,114 @@ +import { ProjectSummary } from './project.types'; + +export const seedProjects: ProjectSummary[] = [ + { + id: 'project_access_manage', + key: 'access-manage', + name: 'access-manage', + repositoryUrl: 'https://gitea.ops.mrzhan.top/my-project/access-manage.git', + defaultBranch: 'develop', + status: 'active', + environments: [ + { + name: 'test', + displayName: '测试环境', + jenkinsJobPath: 'my-project-dev/test-access-manage-develop', + }, + { + name: 'production', + displayName: '生产环境', + jenkinsJobPath: 'my-project-prod/prod-access-manage-tag', + }, + ], + }, + { + id: 'project_role_admin', + key: 'role-admin', + name: 'role-admin', + repositoryUrl: 'https://gitea.ops.mrzhan.top/my-project/role-admin.git', + defaultBranch: 'develop', + status: 'active', + environments: [ + { + name: 'test', + displayName: '测试环境', + jenkinsJobPath: 'my-project-dev/test-role-admin-develop', + }, + { + name: 'production', + displayName: '生产环境', + jenkinsJobPath: 'my-project-prod/prod-role-admin-tag', + }, + ], + }, + { + id: 'project_role_user', + key: 'role-user', + name: 'role-user', + repositoryUrl: 'https://gitea.ops.mrzhan.top/my-project/role-user.git', + defaultBranch: 'develop', + status: 'active', + environments: [ + { + name: 'test', + displayName: '测试环境', + jenkinsJobPath: 'my-project-dev/test-role-user-develop', + }, + { + name: 'production', + displayName: '生产环境', + jenkinsJobPath: 'my-project-prod/prod-role-user-tag', + }, + ], + }, + { + id: 'project_my_resume', + key: 'my-resume', + name: 'my-resume', + repositoryUrl: 'https://gitea.ops.mrzhan.top/my-project/my-resume.git', + defaultBranch: 'master', + status: 'active', + environments: [ + { + name: 'test', + displayName: '测试环境', + jenkinsJobPath: 'my-project-dev/test-my-resume-develop', + }, + { + name: 'production', + displayName: '生产环境', + jenkinsJobPath: 'my-project-prod/prod-my-resume-tag', + }, + ], + }, + { + id: 'project_devops_platform_api', + key: 'devops-platform-api', + name: 'devops-platform-api', + repositoryUrl: 'https://gitea.ops.mrzhan.top/devops-platform/devops-platform-api.git', + defaultBranch: 'master', + status: 'active', + environments: [ + { + name: 'production', + displayName: '生产环境', + jenkinsJobPath: 'devops-platform/prod-devops-platform-api-master', + }, + ], + }, + { + id: 'project_devops_platform_web', + key: 'devops-platform-web', + name: 'devops-platform-web', + repositoryUrl: 'https://gitea.ops.mrzhan.top/devops-platform/devops-platform-web.git', + defaultBranch: 'master', + status: 'active', + environments: [ + { + name: 'production', + displayName: '生产环境', + jenkinsJobPath: 'devops-platform/prod-devops-platform-web-master', + }, + ], + }, +]; diff --git a/src/projects/project.types.ts b/src/projects/project.types.ts new file mode 100644 index 0000000..e0e3d14 --- /dev/null +++ b/src/projects/project.types.ts @@ -0,0 +1,27 @@ +export type ProjectEnvironmentSummary = { + name: 'test' | 'production'; + displayName: string; + jenkinsJobPath: string; +}; + +export type ProjectSummary = { + id: string; + key: string; + name: string; + repositoryUrl: string; + defaultBranch: string; + status: 'active' | 'archived'; + environments: ProjectEnvironmentSummary[]; +}; + +export type ProjectRefSummary = { + source: 'gitea' | 'fallback'; + branches: Array<{ + name: string; + commitSha?: string; + }>; + tags: Array<{ + name: string; + commitSha?: string; + }>; +}; diff --git a/src/projects/projects.controller.ts b/src/projects/projects.controller.ts new file mode 100644 index 0000000..2c0a97b --- /dev/null +++ b/src/projects/projects.controller.ts @@ -0,0 +1,28 @@ +import { Controller, Get, Param } from '@nestjs/common'; +import { ApiOkResponse, ApiTags } from '@nestjs/swagger'; +import { ProjectRefSummary, ProjectSummary } from './project.types'; +import { ProjectsService } from './projects.service'; + +@ApiTags('projects') +@Controller('projects') +export class ProjectsController { + constructor(private readonly projectsService: ProjectsService) {} + + @Get() + @ApiOkResponse({ description: 'Configured project list.' }) + async listProjects(): Promise { + return this.projectsService.listProjects(); + } + + @Get(':key') + @ApiOkResponse({ description: 'Project detail by key.' }) + async getProject(@Param('key') key: string): Promise { + return this.projectsService.getProject(key); + } + + @Get(':key/refs') + @ApiOkResponse({ description: 'Project branches and tags.' }) + async listProjectRefs(@Param('key') key: string): Promise { + return this.projectsService.listProjectRefs(key); + } +} diff --git a/src/projects/projects.module.ts b/src/projects/projects.module.ts new file mode 100644 index 0000000..67f18d0 --- /dev/null +++ b/src/projects/projects.module.ts @@ -0,0 +1,13 @@ +import { Module } from '@nestjs/common'; +import { GiteaModule } from '../integrations/gitea/gitea.module'; +import { ProjectRepository } from './project.repository'; +import { ProjectsController } from './projects.controller'; +import { ProjectsService } from './projects.service'; + +@Module({ + imports: [GiteaModule], + controllers: [ProjectsController], + providers: [ProjectRepository, ProjectsService], + exports: [ProjectRepository, ProjectsService], +}) +export class ProjectsModule {} diff --git a/src/projects/projects.service.spec.ts b/src/projects/projects.service.spec.ts new file mode 100644 index 0000000..4ac940b --- /dev/null +++ b/src/projects/projects.service.spec.ts @@ -0,0 +1,84 @@ +import { AppError } from '../common/errors/app-error'; +import { GiteaClient } from '../integrations/gitea/gitea.client'; +import { ProjectRepository } from './project.repository'; +import { ProjectSummary } from './project.types'; +import { ProjectsService } from './projects.service'; + +describe('ProjectsService', () => { + const project: ProjectSummary = { + id: 'project_access_manage', + key: 'access-manage', + name: 'access-manage', + repositoryUrl: 'https://gitea.ops.mrzhan.top/my-project/access-manage.git', + defaultBranch: 'develop', + status: 'active', + environments: [], + }; + + function createService(options?: { + giteaConfigured?: boolean; + branches?: Array<{ name: string; commitSha: string }>; + tags?: Array<{ name: string; commitSha: string }>; + }) { + const repository = { + findMany: jest.fn(() => Promise.resolve([project])), + findByKey: jest.fn((key: string) => + Promise.resolve(key === project.key ? project : null), + ), + } satisfies Pick; + const gitea = { + healthSummary: jest.fn(() => + options?.giteaConfigured ? { status: 'ok' as const } : { status: 'not_configured' as const }, + ), + listBranches: jest.fn(() => + Promise.resolve(options?.branches ?? [{ name: 'develop', commitSha: 'abc' }]), + ), + listTags: jest.fn(() => + Promise.resolve(options?.tags ?? [{ name: 'v1.0.0', commitSha: 'def' }]), + ), + } satisfies Pick; + + return { + gitea, + repository, + service: new ProjectsService( + repository as unknown as ProjectRepository, + gitea as unknown as GiteaClient, + ), + }; + } + + it('returns fallback refs when Gitea is not configured', async () => { + const { gitea, service } = createService({ giteaConfigured: false }); + + const refs = await service.listProjectRefs(project.key); + + expect(refs.source).toBe('fallback'); + expect(refs.branches).toContainEqual({ name: 'develop' }); + expect(refs.tags).toContainEqual({ name: 'v2026.06.05-envsplit.7' }); + expect(gitea.listBranches).not.toHaveBeenCalled(); + }); + + it('returns Gitea refs when Gitea is configured', async () => { + const { gitea, service } = createService({ + giteaConfigured: true, + branches: [{ name: 'feature/devops-platform', commitSha: 'abc123' }], + tags: [{ name: 'v2026.06.11.1', commitSha: 'def456' }], + }); + + const refs = await service.listProjectRefs(project.key); + + expect(refs).toEqual({ + source: 'gitea', + branches: [{ name: 'feature/devops-platform', commitSha: 'abc123' }], + tags: [{ name: 'v2026.06.11.1', commitSha: 'def456' }], + }); + expect(gitea.listBranches).toHaveBeenCalledWith('my-project', 'access-manage'); + }); + + it('raises a not found error for unknown projects', async () => { + const { service } = createService(); + + await expect(service.listProjectRefs('unknown')).rejects.toBeInstanceOf(AppError); + }); +}); diff --git a/src/projects/projects.service.ts b/src/projects/projects.service.ts new file mode 100644 index 0000000..38a1cef --- /dev/null +++ b/src/projects/projects.service.ts @@ -0,0 +1,86 @@ +import { Injectable } from '@nestjs/common'; +import { AppError } from '../common/errors/app-error'; +import { GiteaClient } from '../integrations/gitea/gitea.client'; +import { ProjectRepository } from './project.repository'; +import { ProjectRefSummary, ProjectSummary } from './project.types'; + +@Injectable() +export class ProjectsService { + constructor( + private readonly projectRepository: ProjectRepository, + private readonly giteaClient: GiteaClient, + ) {} + + async listProjects(): Promise { + return this.projectRepository.findMany(); + } + + async getProject(key: string): Promise { + const project = await this.projectRepository.findByKey(key); + + if (!project) { + throw new AppError('RESOURCE_NOT_FOUND', `Project ${key} not found`, 404, { + key, + }); + } + + return project; + } + + async listProjectRefs(key: string): Promise { + const project = await this.getProject(key); + const repo = this.parseRepository(project); + + if (repo && this.giteaClient.healthSummary().status === 'ok') { + try { + const [branches, tags] = await Promise.all([ + this.giteaClient.listBranches(repo.owner, repo.name), + this.giteaClient.listTags(repo.owner, repo.name), + ]); + + return { + source: 'gitea', + branches, + tags, + }; + } catch { + return this.fallbackRefs(project); + } + } + + return this.fallbackRefs(project); + } + + private parseRepository( + project: ProjectSummary, + ): { owner: string; name: string } | null { + try { + const pathname = new URL(project.repositoryUrl).pathname.replace(/\.git$/, ''); + const parts = pathname.split('/').filter(Boolean); + const name = parts.at(-1); + const owner = parts.at(-2); + + return owner && name ? { owner, name } : null; + } catch { + return null; + } + } + + private fallbackRefs(project: ProjectSummary): ProjectRefSummary { + const branches = Array.from( + new Set([project.defaultBranch, project.key === 'my-resume' ? 'master' : 'develop']), + ) + .filter(Boolean) + .map((name) => ({ name })); + + return { + source: 'fallback', + branches, + tags: [ + { + name: 'v2026.06.05-envsplit.7', + }, + ], + }; + } +} diff --git a/src/settings/settings.controller.ts b/src/settings/settings.controller.ts new file mode 100644 index 0000000..65ebdcc --- /dev/null +++ b/src/settings/settings.controller.ts @@ -0,0 +1,19 @@ +import { Controller, Get } from '@nestjs/common'; +import { ApiOkResponse, ApiTags } from '@nestjs/swagger'; +import { SettingsService } from './settings.service'; +import { IntegrationConfigStatusResponse } from './settings.types'; + +@ApiTags('settings') +@Controller('settings') +export class SettingsController { + constructor(private readonly settingsService: SettingsService) {} + + @Get('integration-config') + @ApiOkResponse({ + description: + 'Integration configuration status with secret values redacted server-side.', + }) + getIntegrationConfig(): IntegrationConfigStatusResponse { + return this.settingsService.getIntegrationConfigStatus(); + } +} diff --git a/src/settings/settings.module.ts b/src/settings/settings.module.ts new file mode 100644 index 0000000..63e54df --- /dev/null +++ b/src/settings/settings.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; +import { SettingsController } from './settings.controller'; +import { SettingsService } from './settings.service'; + +@Module({ + controllers: [SettingsController], + providers: [SettingsService], +}) +export class SettingsModule {} diff --git a/src/settings/settings.service.spec.ts b/src/settings/settings.service.spec.ts new file mode 100644 index 0000000..6f266e1 --- /dev/null +++ b/src/settings/settings.service.spec.ts @@ -0,0 +1,165 @@ +import { ConfigService } from '@nestjs/config'; +import { EnvConfig } from '../config/env.schema'; +import { SettingsService } from './settings.service'; + +describe('SettingsService', () => { + it('returns configured when every required integration variable is real', () => { + const secretValues = [ + 'jenkins-real-token', + 'gitea-real-token', + 'gitea-real-webhook-secret', + 'wecom-real-key', + 'llm-real-api-key', + '0123456789abcdef0123456789abcdef', + ]; + const service = new SettingsService( + createConfig({ + DATABASE_URL: 'mysql://devops_user:local-password@mysql.internal:3306/devops', + REDIS_URL: 'redis://redis.internal:6379/0', + JENKINS_BASE_URL: 'https://jenkins.internal', + JENKINS_USERNAME: 'devops-bot', + JENKINS_API_TOKEN: secretValues[0], + GITEA_BASE_URL: 'https://gitea.internal', + GITEA_TOKEN: secretValues[1], + GITEA_WEBHOOK_SECRET: secretValues[2], + NOTIFICATION_PROVIDER: 'wecom', + WECOM_WEBHOOK_URL: `https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=${secretValues[3]}`, + LLM_BASE_URL: 'https://llm.internal/v1', + LLM_API_KEY: secretValues[4], + LLM_MODEL: 'gpt-enterprise', + SECRET_ENCRYPTION_KEY: secretValues[5], + }), + ); + + const result = service.getIntegrationConfigStatus(); + + expect(Date.parse(result.checkedAt)).not.toBeNaN(); + expect(result.integrations).toHaveLength(7); + expect(result.integrations.map((integration) => integration.status)).toEqual( + Array(7).fill('configured'), + ); + }); + + it('marks partially configured integrations as partial with missing variable names only', () => { + const service = new SettingsService( + createConfig({ + LLM_BASE_URL: 'https://llm.internal/v1', + LLM_MODEL: 'gpt-enterprise', + }), + ); + + const llm = service + .getIntegrationConfigStatus() + .integrations.find((integration) => integration.key === 'llm'); + + expect(llm).toMatchObject({ + status: 'partial', + configured: ['LLM_BASE_URL', 'LLM_MODEL'], + missing: ['LLM_API_KEY'], + }); + }); + + it('treats env example placeholders as missing values', () => { + const service = new SettingsService( + createConfig({ + JENKINS_BASE_URL: 'https://jenkins.example.internal', + JENKINS_USERNAME: 'devops-bot', + JENKINS_API_TOKEN: 'replace-with-secret-manager-value', + GITEA_BASE_URL: 'https://gitea.example.internal', + GITEA_TOKEN: 'replace-with-secret-manager-value', + GITEA_WEBHOOK_SECRET: 'replace-with-secret-manager-value', + NOTIFICATION_PROVIDER: 'replace-with-provider', + WECOM_WEBHOOK_URL: + 'https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=replace-with-secret-manager-value', + LLM_BASE_URL: 'https://llm.example.internal/v1', + LLM_API_KEY: 'replace-with-secret-manager-value', + LLM_MODEL: 'replace-with-model-name', + SECRET_ENCRYPTION_KEY: 'replace-with-32-byte-key', + }), + ); + + const result = service.getIntegrationConfigStatus(); + const byKey = Object.fromEntries( + result.integrations.map((integration) => [integration.key, integration]), + ); + + expect(byKey.jenkins).toMatchObject({ + status: 'partial', + configured: ['JENKINS_USERNAME'], + missing: ['JENKINS_BASE_URL', 'JENKINS_API_TOKEN'], + }); + expect(byKey.gitea).toMatchObject({ + status: 'missing', + missing: ['GITEA_BASE_URL', 'GITEA_TOKEN'], + }); + expect(byKey.notification).toMatchObject({ + status: 'missing', + missing: ['NOTIFICATION_PROVIDER'], + }); + expect(byKey.llm).toMatchObject({ + status: 'missing', + missing: ['LLM_BASE_URL', 'LLM_API_KEY', 'LLM_MODEL'], + }); + expect(byKey.secrets).toMatchObject({ + status: 'missing', + missing: ['SECRET_ENCRYPTION_KEY'], + }); + }); + + it('does not serialize secret, token, base URL, or webhook values', () => { + const hiddenValues = [ + 'https://jenkins.internal', + 'jenkins-real-token', + 'https://gitea.internal', + 'gitea-real-token', + 'gitea-real-webhook-secret', + 'https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=wecom-real-key', + 'https://open.feishu.cn/open-apis/bot/v2/hook/feishu-real-key', + 'https://notification.internal/webhook/generic-real-key', + 'https://llm.internal/v1', + 'llm-real-api-key', + 'mysql://devops_user:local-password@mysql.internal:3306/devops', + 'redis://redis.internal:6379/0', + '0123456789abcdef0123456789abcdef', + ]; + const service = new SettingsService( + createConfig({ + DATABASE_URL: hiddenValues[10], + REDIS_URL: hiddenValues[11], + JENKINS_BASE_URL: hiddenValues[0], + JENKINS_USERNAME: 'devops-bot', + JENKINS_API_TOKEN: hiddenValues[1], + GITEA_BASE_URL: hiddenValues[2], + GITEA_TOKEN: hiddenValues[3], + GITEA_WEBHOOK_SECRET: hiddenValues[4], + WECOM_WEBHOOK_URL: hiddenValues[5], + FEISHU_WEBHOOK_URL: hiddenValues[6], + NOTIFICATION_WEBHOOK_URL: hiddenValues[7], + NOTIFICATION_PROVIDER: 'wecom', + LLM_BASE_URL: hiddenValues[8], + LLM_API_KEY: hiddenValues[9], + LLM_MODEL: 'gpt-enterprise', + SECRET_ENCRYPTION_KEY: hiddenValues[12], + }), + ); + + const serialized = JSON.stringify(service.getIntegrationConfigStatus()); + + hiddenValues.forEach((value) => { + expect(serialized).not.toContain(value); + }); + expect(serialized).toContain('JENKINS_API_TOKEN'); + expect(serialized).toContain('NOTIFICATION_PROVIDER'); + expect(serialized).toContain('WECOM_WEBHOOK_URL'); + expect(serialized).toContain('FEISHU_WEBHOOK_URL'); + expect(serialized).toContain('SECRET_ENCRYPTION_KEY'); + }); +}); + +function createConfig( + values: Partial>, +): ConfigService { + return { + get: jest.fn((key: keyof EnvConfig) => values[key]), + } as unknown as ConfigService; +} diff --git a/src/settings/settings.service.ts b/src/settings/settings.service.ts new file mode 100644 index 0000000..8140fb0 --- /dev/null +++ b/src/settings/settings.service.ts @@ -0,0 +1,133 @@ +import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { EnvConfig } from '../config/env.schema'; +import { + IntegrationConfigKey, + IntegrationConfigState, + IntegrationConfigStatus, + IntegrationConfigStatusResponse, +} from './settings.types'; + +type EnvStringKey = { + [Key in keyof EnvConfig]-?: Extract extends never + ? never + : Key; +}[keyof EnvConfig]; +type EnvKey = keyof EnvConfig; + +type IntegrationDefinition = { + key: IntegrationConfigKey; + name: string; + required: EnvStringKey[]; + optional?: EnvKey[]; + note?: string; +}; + +const INTEGRATION_DEFINITIONS: IntegrationDefinition[] = [ + { + key: 'jenkins', + name: 'Jenkins', + required: ['JENKINS_BASE_URL', 'JENKINS_USERNAME', 'JENKINS_API_TOKEN'], + }, + { + key: 'gitea', + name: 'Gitea', + required: ['GITEA_BASE_URL', 'GITEA_TOKEN'], + optional: ['GITEA_WEBHOOK_SECRET'], + note: 'GITEA_WEBHOOK_SECRET 用于开启 webhook 签名校验。', + }, + { + key: 'notification', + name: '通知平台', + required: ['NOTIFICATION_PROVIDER'], + optional: ['WECOM_WEBHOOK_URL', 'FEISHU_WEBHOOK_URL', 'NOTIFICATION_WEBHOOK_URL'], + note: '当前不强制配置机器人;后续可按 provider 接入企微、飞书或通用 webhook。', + }, + { + key: 'llm', + name: 'LLM Agent', + required: ['LLM_BASE_URL', 'LLM_API_KEY', 'LLM_MODEL'], + }, + { + key: 'database', + name: 'MySQL / Prisma', + required: ['DATABASE_URL'], + optional: ['USE_DATABASE_READS', 'PRISMA_CONNECT_ON_BOOT'], + }, + { + key: 'redis', + name: 'Redis / BullMQ', + required: ['REDIS_URL'], + }, + { + key: 'secrets', + name: '服务端密钥加密', + required: ['SECRET_ENCRYPTION_KEY'], + }, +]; + +const PLACEHOLDER_PATTERNS = [ + /replace-with-/i, + /\.example\.internal(?:[/:]|$)/i, +]; + +@Injectable() +export class SettingsService { + constructor(private readonly config: ConfigService) {} + + getIntegrationConfigStatus(): IntegrationConfigStatusResponse { + return { + checkedAt: new Date().toISOString(), + integrations: INTEGRATION_DEFINITIONS.map((definition) => + this.toIntegrationStatus(definition), + ), + }; + } + + private toIntegrationStatus( + definition: IntegrationDefinition, + ): IntegrationConfigStatus { + const configured = definition.required.filter((key) => + this.isConfiguredValue(this.getConfigValue(key)), + ); + const missing = definition.required.filter( + (key) => !this.isConfiguredValue(this.getConfigValue(key)), + ); + + return { + key: definition.key, + name: definition.name, + status: this.resolveStatus(configured.length, missing.length), + required: [...definition.required], + configured, + missing, + ...(definition.optional ? { optional: [...definition.optional] } : {}), + ...(definition.note ? { note: definition.note } : {}), + }; + } + + private getConfigValue(key: EnvStringKey): string | undefined { + const value = this.config.get(key, { infer: true }); + + return typeof value === 'string' ? value.trim() : undefined; + } + + private isConfiguredValue(value: string | undefined): boolean { + if (!value) { + return false; + } + + return !PLACEHOLDER_PATTERNS.some((pattern) => pattern.test(value)); + } + + private resolveStatus( + configuredCount: number, + missingCount: number, + ): IntegrationConfigState { + if (missingCount === 0) { + return 'configured'; + } + + return configuredCount > 0 ? 'partial' : 'missing'; + } +} diff --git a/src/settings/settings.types.ts b/src/settings/settings.types.ts new file mode 100644 index 0000000..56009e9 --- /dev/null +++ b/src/settings/settings.types.ts @@ -0,0 +1,27 @@ +export type IntegrationConfigKey = + | 'jenkins' + | 'gitea' + | 'notification' + | 'wecom' + | 'llm' + | 'database' + | 'redis' + | 'secrets'; + +export type IntegrationConfigState = 'configured' | 'partial' | 'missing'; + +export type IntegrationConfigStatus = { + key: IntegrationConfigKey; + name: string; + status: IntegrationConfigState; + required: string[]; + configured: string[]; + missing: string[]; + optional?: string[]; + note?: string; +}; + +export type IntegrationConfigStatusResponse = { + checkedAt: string; + integrations: IntegrationConfigStatus[]; +}; diff --git a/src/webhooks/gitea/gitea-signature.spec.ts b/src/webhooks/gitea/gitea-signature.spec.ts new file mode 100644 index 0000000..a02932c --- /dev/null +++ b/src/webhooks/gitea/gitea-signature.spec.ts @@ -0,0 +1,39 @@ +import { createHmac } from 'node:crypto'; +import { verifyGiteaSignature } from './gitea-signature'; + +/** + * Gitea webhook 签名必须基于原始 body,兼容两种常见签名头格式。 + */ +describe('verifyGiteaSignature', () => { + const rawBody = Buffer.from(JSON.stringify({ ref: 'refs/heads/develop' })); + const secret = 'webhook-secret'; + const hex = createHmac('sha256', secret).update(rawBody).digest('hex'); + + it('accepts unsigned webhooks when secret is not configured', () => { + expect(verifyGiteaSignature({ rawBody })).toBe(true); + }); + + it('accepts X-Gitea-Signature hex value', () => { + expect(verifyGiteaSignature({ rawBody, secret, signature: hex })).toBe(true); + }); + + it('accepts X-Hub-Signature-256 value', () => { + expect( + verifyGiteaSignature({ + rawBody, + secret, + hubSignature256: `sha256=${hex}`, + }), + ).toBe(true); + }); + + it('rejects mismatched signatures', () => { + expect( + verifyGiteaSignature({ + rawBody, + secret, + signature: 'bad-signature', + }), + ).toBe(false); + }); +}); diff --git a/src/webhooks/gitea/gitea-signature.ts b/src/webhooks/gitea/gitea-signature.ts new file mode 100644 index 0000000..c85677b --- /dev/null +++ b/src/webhooks/gitea/gitea-signature.ts @@ -0,0 +1,53 @@ +import { createHmac, timingSafeEqual } from 'node:crypto'; + +/** + * 校验 Gitea webhook HMAC-SHA256 签名,兼容 X-Gitea-Signature 与 GitHub 风格头。 + */ +export function verifyGiteaSignature(input: { + rawBody?: Buffer; + secret?: string; + signature?: string; + hubSignature256?: string; +}): boolean { + if (!input.secret) { + return true; + } + + if (!input.rawBody) { + return false; + } + + const expectedHex = createHmac('sha256', input.secret) + .update(input.rawBody) + .digest('hex'); + const candidates = [ + normalizeSignature(input.signature), + normalizeSignature(input.hubSignature256), + ].filter(Boolean); + + return candidates.some((candidate) => safeEqualHex(expectedHex, candidate)); +} + +function normalizeSignature(signature?: string): string | undefined { + if (!signature) { + return undefined; + } + + const trimmed = signature.trim(); + return trimmed.startsWith('sha256=') ? trimmed.slice('sha256='.length) : trimmed; +} + +function safeEqualHex(left: string, right?: string): boolean { + if (!right || !/^[a-f0-9]+$/i.test(right)) { + return false; + } + + const leftBuffer = Buffer.from(left, 'hex'); + const rightBuffer = Buffer.from(right, 'hex'); + + if (leftBuffer.length !== rightBuffer.length) { + return false; + } + + return timingSafeEqual(leftBuffer, rightBuffer); +} diff --git a/src/webhooks/gitea/gitea-webhook.controller.ts b/src/webhooks/gitea/gitea-webhook.controller.ts new file mode 100644 index 0000000..f9ff49a --- /dev/null +++ b/src/webhooks/gitea/gitea-webhook.controller.ts @@ -0,0 +1,40 @@ +import { + Body, + Controller, + Headers, + Post, + RawBody, +} from '@nestjs/common'; +import { ApiCreatedResponse, ApiTags } from '@nestjs/swagger'; +import { GiteaWebhookHeaders, GiteaWebhookResult } from './gitea-webhook.types'; +import { GiteaWebhookService } from './gitea-webhook.service'; + +@ApiTags('webhooks') +@Controller('webhooks/gitea') +export class GiteaWebhookController { + constructor(private readonly giteaWebhookService: GiteaWebhookService) {} + + @Post() + @ApiCreatedResponse({ description: 'Receive a Gitea webhook event.' }) + async receiveWebhook( + @Headers('x-gitea-event') event: string | undefined, + @Headers('x-gitea-delivery') delivery: string | undefined, + @Headers('x-gitea-signature') signature: string | undefined, + @Headers('x-hub-signature-256') hubSignature256: string | undefined, + @Body() payload: unknown, + @RawBody() rawBody?: Buffer, + ): Promise { + const headers: GiteaWebhookHeaders = { + event, + delivery, + signature, + hubSignature256, + }; + + return this.giteaWebhookService.handleWebhook({ + headers, + payload, + rawBody, + }); + } +} diff --git a/src/webhooks/gitea/gitea-webhook.module.ts b/src/webhooks/gitea/gitea-webhook.module.ts new file mode 100644 index 0000000..3edd8e1 --- /dev/null +++ b/src/webhooks/gitea/gitea-webhook.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { DeployRunsModule } from '../../deploy-runs/deploy-runs.module'; +import { ProjectsModule } from '../../projects/projects.module'; +import { GiteaWebhookController } from './gitea-webhook.controller'; +import { GiteaWebhookService } from './gitea-webhook.service'; + +@Module({ + imports: [ProjectsModule, DeployRunsModule], + controllers: [GiteaWebhookController], + providers: [GiteaWebhookService], +}) +export class GiteaWebhookModule {} diff --git a/src/webhooks/gitea/gitea-webhook.service.spec.ts b/src/webhooks/gitea/gitea-webhook.service.spec.ts new file mode 100644 index 0000000..4a2d3b8 --- /dev/null +++ b/src/webhooks/gitea/gitea-webhook.service.spec.ts @@ -0,0 +1,138 @@ +import { ConfigService } from '@nestjs/config'; +import { AuditService } from '../../audit/audit.service'; +import { AppError } from '../../common/errors/app-error'; +import { EnvConfig } from '../../config/env.schema'; +import { DeployRunsService } from '../../deploy-runs/deploy-runs.service'; +import { ProjectsService } from '../../projects/projects.service'; +import { GiteaWebhookService } from './gitea-webhook.service'; + +/** + * Gitea webhook 是测试环境自动部署入口,必须只接受受管仓库的分支 push。 + */ +describe('GiteaWebhookService', () => { + const pushPayload = { + ref: 'refs/heads/develop', + after: 'abc123', + repository: { + name: 'access-manage', + full_name: 'my-project/access-manage', + }, + sender: { + login: 'developer', + }, + head_commit: { + message: 'feat: smoke deploy', + }, + }; + + function createService(secret?: string) { + const config = { + get: jest.fn((key: keyof EnvConfig) => + key === 'GITEA_WEBHOOK_SECRET' ? secret : undefined, + ), + } satisfies Pick, 'get'>; + const projects = { + listProjects: jest.fn(() => + Promise.resolve([ + { + id: 'project_access_manage', + key: 'access-manage', + name: 'access-manage', + repositoryUrl: + 'https://gitea.ops.mrzhan.top/my-project/access-manage.git', + defaultBranch: 'develop', + status: 'active' as const, + environments: [], + }, + ]), + ), + } satisfies Pick; + const deployRuns = { + createRun: jest.fn(() => + Promise.resolve({ + id: 'gitea:delivery-1:access-manage:develop:abc123', + projectKey: 'access-manage', + environment: 'test' as const, + ref: 'develop', + status: 'pending' as const, + trigger: 'gitea_webhook' as const, + operator: 'gitea:developer', + createdAt: '2026-06-11T00:00:00.000Z', + steps: [], + }), + ), + } satisfies Pick; + const audit = { + record: jest.fn(), + } satisfies Pick; + + return { + audit, + deployRuns, + projects, + service: new GiteaWebhookService( + config as unknown as ConfigService, + projects as unknown as ProjectsService, + deployRuns as unknown as DeployRunsService, + audit as unknown as AuditService, + ), + }; + } + + it('creates a test deploy run for a managed branch push', async () => { + const { audit, deployRuns, service } = createService(); + + const result = await service.handleWebhook({ + headers: { event: 'push', delivery: 'delivery-1' }, + payload: pushPayload, + rawBody: Buffer.from(JSON.stringify(pushPayload)), + }); + + expect(result.accepted).toBe(true); + expect(result.deployRunId).toBe('gitea:delivery-1:access-manage:develop:abc123'); + expect(deployRuns.createRun).toHaveBeenCalledWith( + expect.objectContaining({ + projectKey: 'access-manage', + environment: 'test', + ref: 'develop', + trigger: 'gitea_webhook', + }), + ); + expect(audit.record).toHaveBeenCalledWith( + expect.objectContaining({ action: 'GITEA_WEBHOOK_ACCEPTED' }), + ); + }); + + it('ignores tag push events', async () => { + const { deployRuns, service } = createService(); + + const result = await service.handleWebhook({ + headers: { event: 'push', delivery: 'delivery-2' }, + payload: { + ...pushPayload, + ref: 'refs/tags/v2026.06.11', + }, + rawBody: Buffer.from('{}'), + }); + + expect(result.accepted).toBe(false); + expect(result.reason).toContain('Only branch push'); + expect(deployRuns.createRun).not.toHaveBeenCalled(); + }); + + it('rejects invalid signatures when secret is configured', async () => { + const { service } = createService('webhook-secret'); + + await expect( + service.handleWebhook({ + headers: { + event: 'push', + delivery: 'delivery-3', + signature: 'invalid', + }, + payload: pushPayload, + rawBody: Buffer.from(JSON.stringify(pushPayload)), + }), + ).rejects.toBeInstanceOf(AppError); + }); +}); diff --git a/src/webhooks/gitea/gitea-webhook.service.ts b/src/webhooks/gitea/gitea-webhook.service.ts new file mode 100644 index 0000000..d80c2ed --- /dev/null +++ b/src/webhooks/gitea/gitea-webhook.service.ts @@ -0,0 +1,177 @@ +import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { AuditService } from '../../audit/audit.service'; +import { AppError } from '../../common/errors/app-error'; +import { EnvConfig } from '../../config/env.schema'; +import { DeployRunsService } from '../../deploy-runs/deploy-runs.service'; +import { ProjectsService } from '../../projects/projects.service'; +import { + GiteaPushPayload, + GiteaWebhookHeaders, + GiteaWebhookResult, +} from './gitea-webhook.types'; +import { verifyGiteaSignature } from './gitea-signature'; + +/** + * 将 Gitea push webhook 转换为测试环境发布单,生产发布仍保留人工入口。 + */ +@Injectable() +export class GiteaWebhookService { + constructor( + private readonly config: ConfigService, + private readonly projectsService: ProjectsService, + private readonly deployRunsService: DeployRunsService, + private readonly auditService: AuditService, + ) {} + + async handleWebhook(input: { + headers: GiteaWebhookHeaders; + payload: unknown; + rawBody?: Buffer; + }): Promise { + this.assertSignature(input); + + if (input.headers.event !== 'push') { + return this.ignore(input.headers, 'Only push events can trigger test deploys'); + } + + const payload = this.asPushPayload(input.payload); + + if (!payload.ref.startsWith('refs/heads/')) { + return this.ignore(input.headers, 'Only branch push events trigger test deploys'); + } + + const branch = payload.ref.replace('refs/heads/', ''); + const projects = await this.projectsService.listProjects(); + const project = projects.find((item) => { + const repositoryName = item.repositoryUrl.replace(/\.git$/, '').split('/').pop(); + const fullName = item.repositoryUrl.replace(/\.git$/, '').split('/').slice(-2).join('/'); + + return ( + item.key === payload.repository.name || + repositoryName === payload.repository.name || + fullName === payload.repository.full_name + ); + }); + + if (!project) { + return this.ignore(input.headers, 'Repository is not managed by this platform'); + } + + const allowedBranches = new Set([project.defaultBranch, 'develop', 'master']); + + if (!allowedBranches.has(branch)) { + return this.ignore( + input.headers, + `Branch ${branch} is not configured for automatic test deploy`, + project.key, + branch, + ); + } + + const operator = + payload.sender?.login ?? payload.sender?.username ?? 'gitea-webhook'; + const run = await this.deployRunsService.createRun({ + projectKey: project.key, + environment: 'test', + ref: branch, + operator: `gitea:${operator}`, + remark: payload.head_commit?.message ?? 'Gitea push webhook', + idempotencyKey: [ + 'gitea', + input.headers.delivery ?? 'no-delivery', + project.key, + branch, + payload.after, + ].join(':'), + trigger: 'gitea_webhook', + }); + + this.auditService.record({ + action: 'GITEA_WEBHOOK_ACCEPTED', + resourceType: 'deploy_run', + resourceId: run.id, + actorName: `gitea:${operator}`, + after: { + delivery: input.headers.delivery, + projectKey: project.key, + branch, + after: payload.after, + }, + }); + + return { + accepted: true, + event: input.headers.event, + delivery: input.headers.delivery, + projectKey: project.key, + branch, + deployRunId: run.id, + }; + } + + private assertSignature(input: { + headers: GiteaWebhookHeaders; + rawBody?: Buffer; + }): void { + const secret = this.config.get('GITEA_WEBHOOK_SECRET', { infer: true }); + + if ( + !verifyGiteaSignature({ + secret, + rawBody: input.rawBody, + signature: input.headers.signature, + hubSignature256: input.headers.hubSignature256, + }) + ) { + throw new AppError('VALIDATION_FAILED', 'Invalid Gitea webhook signature', 401); + } + } + + private asPushPayload(payload: unknown): GiteaPushPayload { + if (!isRecord(payload) || !isRecord(payload.repository)) { + throw new AppError('VALIDATION_FAILED', 'Invalid Gitea push payload', 400); + } + + const ref = typeof payload.ref === 'string' ? payload.ref : ''; + const after = typeof payload.after === 'string' ? payload.after : ''; + + if (!ref || !after) { + throw new AppError('VALIDATION_FAILED', 'Gitea push payload missing ref or after', 400); + } + + return payload as GiteaPushPayload; + } + + private ignore( + headers: GiteaWebhookHeaders, + reason: string, + projectKey?: string, + branch?: string, + ): GiteaWebhookResult { + this.auditService.record({ + action: 'GITEA_WEBHOOK_IGNORED', + resourceType: 'gitea_webhook', + resourceId: headers.delivery, + after: { + event: headers.event, + reason, + projectKey, + branch, + }, + }); + + return { + accepted: false, + event: headers.event ?? 'unknown', + delivery: headers.delivery, + reason, + projectKey, + branch, + }; + } +} + +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} diff --git a/src/webhooks/gitea/gitea-webhook.types.ts b/src/webhooks/gitea/gitea-webhook.types.ts new file mode 100644 index 0000000..813218d --- /dev/null +++ b/src/webhooks/gitea/gitea-webhook.types.ts @@ -0,0 +1,39 @@ +/** + * Gitea push webhook 的最小可信字段集合,避免服务层依赖完整平台 payload。 + */ +export type GiteaWebhookHeaders = { + event?: string; + delivery?: string; + signature?: string; + hubSignature256?: string; +}; + +export type GiteaPushPayload = { + ref: string; + after: string; + repository: { + name?: string; + full_name?: string; + clone_url?: string; + html_url?: string; + }; + sender?: { + login?: string; + username?: string; + }; + head_commit?: { + id?: string; + message?: string; + url?: string; + }; +}; + +export type GiteaWebhookResult = { + accepted: boolean; + event: string; + delivery?: string; + reason?: string; + projectKey?: string; + branch?: string; + deployRunId?: string; +}; diff --git a/tsconfig.build.json b/tsconfig.build.json new file mode 100644 index 0000000..2fe1df2 --- /dev/null +++ b/tsconfig.build.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "exclude": ["node_modules", "dist", "test", "**/*spec.ts"] +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..3086cfe --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "module": "commonjs", + "declaration": true, + "removeComments": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "allowSyntheticDefaultImports": true, + "target": "ES2022", + "sourceMap": true, + "outDir": "./dist", + "baseUrl": "./", + "incremental": true, + "strict": true, + "strictPropertyInitialization": false, + "skipLibCheck": true, + "moduleResolution": "node", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "types": ["node", "jest"] + }, + "include": ["src/**/*.ts", "prisma/**/*.ts"], + "exclude": ["node_modules", "dist"] +}