From 66199cc93e8909b4c1f2b7c78cfad3ae2d22dddf Mon Sep 17 00:00:00 2001 From: Developer Date: Sat, 29 Nov 2025 22:12:41 -0800 Subject: [PATCH] feat(backup-service): Implement MPC backup share storage service MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add DDD + Hexagonal architecture with NestJS 11.x - Implement store/retrieve/revoke backup share endpoints - Add AES-256-GCM double encryption for secure storage - Add service-to-service JWT authentication - Add rate limiting (3 retrieves per user per day) - Add comprehensive audit logging - Add test suite (37 unit + 21 mock E2E + 20 real DB E2E = 78 tests) - Add documentation (architecture, API, development, testing, deployment) - Add Docker and Kubernetes deployment configuration - Add Prisma 7.x with @prisma/adapter-pg for PostgreSQL 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../.claude/settings.local.json | 32 + backend/services/backup-service/.dockerignore | 16 + backend/services/backup-service/.env.example | 25 + backend/services/backup-service/.env.test | 21 + backend/services/backup-service/.gitignore | 16 + backend/services/backup-service/.prettierrc | 4 + backend/services/backup-service/Dockerfile | 56 + .../backup-service/IMPLEMENTATION_GUIDE.md | 901 -- backend/services/backup-service/README.md | 98 + .../backup-service/docker-compose.test.yml | 25 + .../backup-service/docker-compose.yml | 53 + backend/services/backup-service/docs/API.md | 613 + .../backup-service/docs/ARCHITECTURE.md | 430 + .../backup-service/docs/DEPLOYMENT.md | 696 + .../backup-service/docs/DEVELOPMENT.md | 513 + .../services/backup-service/docs/README.md | 77 + .../services/backup-service/docs/TESTING.md | 809 ++ .../services/backup-service/eslint.config.mjs | 35 + backend/services/backup-service/nest-cli.json | 8 + .../services/backup-service/package-lock.json | 11226 ++++++++++++++++ backend/services/backup-service/package.json | 106 + .../services/backup-service/prisma.config.ts | 14 + .../backup-service/prisma/schema.prisma | 69 + .../backup-service/scripts/setup-test-db.ts | 89 + .../backup-service/src/api/api.module.ts | 14 + .../controllers/backup-share.controller.ts | 103 + .../src/api/controllers/health.controller.ts | 44 + .../src/api/dto/request/retrieve-share.dto.ts | 25 + .../src/api/dto/request/revoke-share.dto.ts | 22 + .../src/api/dto/request/store-share.dto.ts | 41 + .../src/api/dto/response/share-info.dto.ts | 24 + .../services/backup-service/src/app.module.ts | 35 + .../src/application/application.module.ts | 19 + .../revoke-share/revoke-share.command.ts | 9 + .../revoke-share/revoke-share.handler.ts | 54 + .../store-backup-share.command.ts | 12 + .../store-backup-share.handler.ts | 83 + .../application/errors/application.error.ts | 10 + .../backup-service/src/application/index.ts | 18 + .../get-backup-share.handler.ts | 120 + .../get-backup-share.query.ts | 10 + .../backup-share-application.service.ts | 28 + .../backup-service/src/config/index.ts | 30 + .../src/domain/domain.module.ts | 8 + .../domain/entities/backup-share.entity.ts | 201 + .../src/domain/errors/domain.error.ts | 7 + .../backup-service/src/domain/index.ts | 15 + .../backup-share.repository.interface.ts | 16 + .../domain/value-objects/encrypted-data.vo.ts | 74 + .../src/domain/value-objects/share-id.vo.ts | 29 + .../crypto/aes-encryption.service.ts | 106 + .../src/infrastructure/index.ts | 10 + .../infrastructure/infrastructure.module.ts | 27 + .../persistence/prisma/prisma.service.ts | 52 + .../repositories/audit-log.repository.ts | 70 + .../backup-share.repository.impl.ts | 123 + backend/services/backup-service/src/main.ts | 40 + .../shared/filters/global-exception.filter.ts | 92 + .../src/shared/guards/service-auth.guard.ts | 70 + .../interceptors/audit-log.interceptor.ts | 63 + .../services/backup-service/test/README.md | 132 + .../test/e2e/backup-share-mock.e2e-spec.ts | 433 + .../test/e2e/backup-share.e2e-spec.ts | 517 + .../audit-log-repository.integration.spec.ts | 204 + ...ackup-share-repository.integration.spec.ts | 252 + .../backup-service/test/jest-e2e-db.json | 17 + .../backup-service/test/jest-e2e-mock.json | 15 + .../backup-service/test/jest-e2e.json | 17 + .../backup-service/test/setup/global-setup.ts | 97 + .../test/setup/global-teardown.ts | 29 + .../test/setup/jest-e2e-setup.ts | 36 + .../test/setup/jest-mock-setup.ts | 10 + .../test/setup/test-database.helper.ts | 50 + .../backup-service/test/tsconfig.json | 7 + .../unit/api/backup-share.controller.spec.ts | 212 + .../test/unit/api/health.controller.spec.ts | 88 + .../get-backup-share.handler.spec.ts | 134 + .../store-backup-share.handler.spec.ts | 127 + .../unit/domain/backup-share.entity.spec.ts | 170 + .../test/unit/domain/value-objects.spec.ts | 178 + .../aes-encryption.service.spec.ts | 142 + .../unit/shared/audit-log.interceptor.spec.ts | 192 + .../shared/global-exception.filter.spec.ts | 215 + .../unit/shared/service-auth.guard.spec.ts | 200 + .../test/utils/mock-prisma.service.ts | 185 + .../backup-service/test/utils/test-utils.ts | 90 + .../backup-service/tsconfig.build.json | 4 + backend/services/backup-service/tsconfig.json | 25 + 88 files changed, 20513 insertions(+), 901 deletions(-) create mode 100644 backend/services/backup-service/.claude/settings.local.json create mode 100644 backend/services/backup-service/.dockerignore create mode 100644 backend/services/backup-service/.env.example create mode 100644 backend/services/backup-service/.env.test create mode 100644 backend/services/backup-service/.gitignore create mode 100644 backend/services/backup-service/.prettierrc create mode 100644 backend/services/backup-service/Dockerfile delete mode 100644 backend/services/backup-service/IMPLEMENTATION_GUIDE.md create mode 100644 backend/services/backup-service/README.md create mode 100644 backend/services/backup-service/docker-compose.test.yml create mode 100644 backend/services/backup-service/docker-compose.yml create mode 100644 backend/services/backup-service/docs/API.md create mode 100644 backend/services/backup-service/docs/ARCHITECTURE.md create mode 100644 backend/services/backup-service/docs/DEPLOYMENT.md create mode 100644 backend/services/backup-service/docs/DEVELOPMENT.md create mode 100644 backend/services/backup-service/docs/README.md create mode 100644 backend/services/backup-service/docs/TESTING.md create mode 100644 backend/services/backup-service/eslint.config.mjs create mode 100644 backend/services/backup-service/nest-cli.json create mode 100644 backend/services/backup-service/package-lock.json create mode 100644 backend/services/backup-service/package.json create mode 100644 backend/services/backup-service/prisma.config.ts create mode 100644 backend/services/backup-service/prisma/schema.prisma create mode 100644 backend/services/backup-service/scripts/setup-test-db.ts create mode 100644 backend/services/backup-service/src/api/api.module.ts create mode 100644 backend/services/backup-service/src/api/controllers/backup-share.controller.ts create mode 100644 backend/services/backup-service/src/api/controllers/health.controller.ts create mode 100644 backend/services/backup-service/src/api/dto/request/retrieve-share.dto.ts create mode 100644 backend/services/backup-service/src/api/dto/request/revoke-share.dto.ts create mode 100644 backend/services/backup-service/src/api/dto/request/store-share.dto.ts create mode 100644 backend/services/backup-service/src/api/dto/response/share-info.dto.ts create mode 100644 backend/services/backup-service/src/app.module.ts create mode 100644 backend/services/backup-service/src/application/application.module.ts create mode 100644 backend/services/backup-service/src/application/commands/revoke-share/revoke-share.command.ts create mode 100644 backend/services/backup-service/src/application/commands/revoke-share/revoke-share.handler.ts create mode 100644 backend/services/backup-service/src/application/commands/store-backup-share/store-backup-share.command.ts create mode 100644 backend/services/backup-service/src/application/commands/store-backup-share/store-backup-share.handler.ts create mode 100644 backend/services/backup-service/src/application/errors/application.error.ts create mode 100644 backend/services/backup-service/src/application/index.ts create mode 100644 backend/services/backup-service/src/application/queries/get-backup-share/get-backup-share.handler.ts create mode 100644 backend/services/backup-service/src/application/queries/get-backup-share/get-backup-share.query.ts create mode 100644 backend/services/backup-service/src/application/services/backup-share-application.service.ts create mode 100644 backend/services/backup-service/src/config/index.ts create mode 100644 backend/services/backup-service/src/domain/domain.module.ts create mode 100644 backend/services/backup-service/src/domain/entities/backup-share.entity.ts create mode 100644 backend/services/backup-service/src/domain/errors/domain.error.ts create mode 100644 backend/services/backup-service/src/domain/index.ts create mode 100644 backend/services/backup-service/src/domain/repositories/backup-share.repository.interface.ts create mode 100644 backend/services/backup-service/src/domain/value-objects/encrypted-data.vo.ts create mode 100644 backend/services/backup-service/src/domain/value-objects/share-id.vo.ts create mode 100644 backend/services/backup-service/src/infrastructure/crypto/aes-encryption.service.ts create mode 100644 backend/services/backup-service/src/infrastructure/index.ts create mode 100644 backend/services/backup-service/src/infrastructure/infrastructure.module.ts create mode 100644 backend/services/backup-service/src/infrastructure/persistence/prisma/prisma.service.ts create mode 100644 backend/services/backup-service/src/infrastructure/persistence/repositories/audit-log.repository.ts create mode 100644 backend/services/backup-service/src/infrastructure/persistence/repositories/backup-share.repository.impl.ts create mode 100644 backend/services/backup-service/src/main.ts create mode 100644 backend/services/backup-service/src/shared/filters/global-exception.filter.ts create mode 100644 backend/services/backup-service/src/shared/guards/service-auth.guard.ts create mode 100644 backend/services/backup-service/src/shared/interceptors/audit-log.interceptor.ts create mode 100644 backend/services/backup-service/test/README.md create mode 100644 backend/services/backup-service/test/e2e/backup-share-mock.e2e-spec.ts create mode 100644 backend/services/backup-service/test/e2e/backup-share.e2e-spec.ts create mode 100644 backend/services/backup-service/test/integration/audit-log-repository.integration.spec.ts create mode 100644 backend/services/backup-service/test/integration/backup-share-repository.integration.spec.ts create mode 100644 backend/services/backup-service/test/jest-e2e-db.json create mode 100644 backend/services/backup-service/test/jest-e2e-mock.json create mode 100644 backend/services/backup-service/test/jest-e2e.json create mode 100644 backend/services/backup-service/test/setup/global-setup.ts create mode 100644 backend/services/backup-service/test/setup/global-teardown.ts create mode 100644 backend/services/backup-service/test/setup/jest-e2e-setup.ts create mode 100644 backend/services/backup-service/test/setup/jest-mock-setup.ts create mode 100644 backend/services/backup-service/test/setup/test-database.helper.ts create mode 100644 backend/services/backup-service/test/tsconfig.json create mode 100644 backend/services/backup-service/test/unit/api/backup-share.controller.spec.ts create mode 100644 backend/services/backup-service/test/unit/api/health.controller.spec.ts create mode 100644 backend/services/backup-service/test/unit/application/get-backup-share.handler.spec.ts create mode 100644 backend/services/backup-service/test/unit/application/store-backup-share.handler.spec.ts create mode 100644 backend/services/backup-service/test/unit/domain/backup-share.entity.spec.ts create mode 100644 backend/services/backup-service/test/unit/domain/value-objects.spec.ts create mode 100644 backend/services/backup-service/test/unit/infrastructure/aes-encryption.service.spec.ts create mode 100644 backend/services/backup-service/test/unit/shared/audit-log.interceptor.spec.ts create mode 100644 backend/services/backup-service/test/unit/shared/global-exception.filter.spec.ts create mode 100644 backend/services/backup-service/test/unit/shared/service-auth.guard.spec.ts create mode 100644 backend/services/backup-service/test/utils/mock-prisma.service.ts create mode 100644 backend/services/backup-service/test/utils/test-utils.ts create mode 100644 backend/services/backup-service/tsconfig.build.json create mode 100644 backend/services/backup-service/tsconfig.json diff --git a/backend/services/backup-service/.claude/settings.local.json b/backend/services/backup-service/.claude/settings.local.json new file mode 100644 index 00000000..21869c6c --- /dev/null +++ b/backend/services/backup-service/.claude/settings.local.json @@ -0,0 +1,32 @@ +{ + "permissions": { + "allow": [ + "Bash(npx @nestjs/cli new:*)", + "Bash(npm install:*)", + "Bash(npx prisma:*)", + "Bash(npm run build:*)", + "Bash(npm run test:unit:*)", + "Bash(npm test:*)", + "Bash(npm run test:e2e:mock:*)", + "Bash(docker info:*)", + "Bash(where:*)", + "Bash(npx ts-node:*)", + "Bash(node -e:*)", + "Bash(npm run test:all:*)", + "Bash(wsl docker info:*)", + "Bash(npx dotenv:*)", + "Bash(wsl docker ps:*)", + "Bash(wsl bash:*)", + "Bash(set \"PRISMA_USER_CONSENT_FOR_DANGEROUS_AI_ACTION=yes\")", + "Bash(wsl docker logs:*)", + "Bash(wsl hostname:*)", + "Bash(timeout:*)", + "Bash(findstr:*)", + "Bash(tree:*)", + "Bash(powershell -Command:*)", + "Bash(git add:*)" + ], + "deny": [], + "ask": [] + } +} diff --git a/backend/services/backup-service/.dockerignore b/backend/services/backup-service/.dockerignore new file mode 100644 index 00000000..d9bfe2eb --- /dev/null +++ b/backend/services/backup-service/.dockerignore @@ -0,0 +1,16 @@ +node_modules +dist +.git +.gitignore +.env +.env.* +!.env.example +*.md +!README.md +test +coverage +.nyc_output +.vscode +.idea +*.log +npm-debug.log* diff --git a/backend/services/backup-service/.env.example b/backend/services/backup-service/.env.example new file mode 100644 index 00000000..6d8963db --- /dev/null +++ b/backend/services/backup-service/.env.example @@ -0,0 +1,25 @@ +# Database (必须是独立的数据库实例,不能与 identity-service 共享!) +DATABASE_URL="postgresql://postgres:password@backup-db-server:5432/rwa_backup?schema=public" + +# Server +APP_PORT=3002 +APP_ENV="development" + +# Service Authentication +SERVICE_JWT_SECRET="your-super-secret-service-jwt-key" +ALLOWED_SERVICES="identity-service,recovery-service" + +# Encryption (用于二次加密备份分片) +BACKUP_ENCRYPTION_KEY="your-256-bit-encryption-key-in-hex" +BACKUP_ENCRYPTION_KEY_ID="key-v1" + +# Rate Limiting +MAX_RETRIEVE_PER_DAY=3 # 每用户每天最多获取 3 次 +MAX_STORE_PER_MINUTE=10 # 每分钟最多存储 10 个 + +# Audit +AUDIT_LOG_RETENTION_DAYS=365 # 审计日志保留 365 天 + +# Monitoring +PROMETHEUS_ENABLED=true +PROMETHEUS_PORT=9102 diff --git a/backend/services/backup-service/.env.test b/backend/services/backup-service/.env.test new file mode 100644 index 00000000..f1e6412d --- /dev/null +++ b/backend/services/backup-service/.env.test @@ -0,0 +1,21 @@ +# Test Environment Configuration +DATABASE_URL="postgresql://postgres:testpassword@localhost:5434/rwa_backup_test?schema=public" + +# Server +APP_PORT=3003 +APP_ENV="test" + +# Service Authentication +SERVICE_JWT_SECRET="test-super-secret-service-jwt-key-for-e2e-testing" +ALLOWED_SERVICES="identity-service,recovery-service" + +# Encryption +BACKUP_ENCRYPTION_KEY="0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" +BACKUP_ENCRYPTION_KEY_ID="test-key-v1" + +# Rate Limiting (higher for tests) +MAX_RETRIEVE_PER_DAY=100 +MAX_STORE_PER_MINUTE=100 + +# Audit +AUDIT_LOG_RETENTION_DAYS=1 diff --git a/backend/services/backup-service/.gitignore b/backend/services/backup-service/.gitignore new file mode 100644 index 00000000..b4b36ea4 --- /dev/null +++ b/backend/services/backup-service/.gitignore @@ -0,0 +1,16 @@ +node_modules +dist +coverage + +# Keep environment variables out of version control +.env + +/generated/prisma + +# Windows artifacts +nul +*.log + +# IDE +.vscode +.idea diff --git a/backend/services/backup-service/.prettierrc b/backend/services/backup-service/.prettierrc new file mode 100644 index 00000000..a20502b7 --- /dev/null +++ b/backend/services/backup-service/.prettierrc @@ -0,0 +1,4 @@ +{ + "singleQuote": true, + "trailingComma": "all" +} diff --git a/backend/services/backup-service/Dockerfile b/backend/services/backup-service/Dockerfile new file mode 100644 index 00000000..a34bbcb4 --- /dev/null +++ b/backend/services/backup-service/Dockerfile @@ -0,0 +1,56 @@ +# Stage 1: Build +FROM node:20-alpine AS builder + +WORKDIR /app + +# Copy package files +COPY package*.json ./ +COPY prisma ./prisma/ + +# Install dependencies +RUN npm ci + +# Copy source code +COPY . . + +# Generate Prisma client +RUN npx prisma generate + +# Build the application +RUN npm run build + +# Stage 2: Production +FROM node:20-alpine AS production + +WORKDIR /app + +# Create non-root user for security +RUN addgroup -g 1001 -S nodejs && \ + adduser -S nestjs -u 1001 + +# Copy package files +COPY package*.json ./ + +# Install production dependencies only +RUN npm ci --only=production && npm cache clean --force + +# Copy built application +COPY --from=builder /app/dist ./dist +COPY --from=builder /app/node_modules/.prisma ./node_modules/.prisma +COPY --from=builder /app/prisma ./prisma + +# Set ownership +RUN chown -R nestjs:nodejs /app + +# Switch to non-root user +USER nestjs + +# Expose port +EXPOSE 3002 + +# Health check +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD wget --no-verbose --tries=1 --spider http://localhost:3002/health || exit 1 + +# Start the application +CMD ["node", "dist/main.js"] diff --git a/backend/services/backup-service/IMPLEMENTATION_GUIDE.md b/backend/services/backup-service/IMPLEMENTATION_GUIDE.md deleted file mode 100644 index 1117bc14..00000000 --- a/backend/services/backup-service/IMPLEMENTATION_GUIDE.md +++ /dev/null @@ -1,901 +0,0 @@ -# Backup Service 实施指导 - -## 1. 概述 - -### 1.1 服务定位 - -`backup-service` 是 RWA Durian 平台的 MPC 备份分片存储服务,负责安全存储用户的 Backup Share (Party 2)。 - -**核心职责:** -- 安全存储 MPC 2-of-3 的第三个分片 (Backup Share) -- 提供分片的存取 API -- 支持用户账户恢复时的分片验证和获取 -- 审计日志记录 - -### 1.2 安全要求 - -**关键安全原则:必须部署在与 identity-service 不同的物理服务器上!** - -``` -┌──────────────────────────────────────────────────────────────────┐ -│ MPC 分片物理隔离架构 │ -├──────────────────────────────────────────────────────────────────┤ -│ │ -│ 物理服务器 A 物理服务器 B 用户设备 │ -│ ┌─────────────────┐ ┌─────────────────┐ ┌────────────┐ │ -│ │identity-service │ │ backup-service │ │Flutter App │ │ -│ │Server Share (0) │ │Backup Share (2) │ │Client (1) │ │ -│ └─────────────────┘ └─────────────────┘ └────────────┘ │ -│ │ -│ 任意单点被攻破,攻击者只能获得 1 个分片,无法重建私钥 │ -└──────────────────────────────────────────────────────────────────┘ -``` - -如果 Server Share 和 Backup Share 在同一台物理服务器,攻击者攻破该服务器后可获得 2 个分片,直接重建私钥,**MPC 安全性完全失效**! - -### 1.3 技术栈 - -与 identity-service 保持一致: -- **框架**: NestJS 10.x -- **语言**: TypeScript 5.x -- **数据库**: PostgreSQL 15+ (独立数据库实例) -- **ORM**: Prisma 5.x -- **缓存**: Redis (可选) -- **消息队列**: Kafka (可选,用于接收 identity-service 的事件) - ---- - -## 2. 目录结构 - -参考 identity-service 的 DDD + Hexagonal 架构: - -``` -backup-service/ -├── prisma/ -│ ├── schema.prisma # 数据库模型 -│ └── migrations/ # 数据库迁移 -├── src/ -│ ├── api/ # 接口层 (Adapters - Driving) -│ │ ├── controllers/ -│ │ │ └── backup-share.controller.ts -│ │ ├── dto/ -│ │ │ ├── request/ -│ │ │ │ ├── store-share.dto.ts -│ │ │ │ └── retrieve-share.dto.ts -│ │ │ └── response/ -│ │ │ └── share-info.dto.ts -│ │ └── api.module.ts -│ │ -│ ├── application/ # 应用层 (Use Cases) -│ │ ├── commands/ -│ │ │ ├── store-backup-share/ -│ │ │ │ ├── store-backup-share.command.ts -│ │ │ │ └── store-backup-share.handler.ts -│ │ │ └── revoke-share/ -│ │ │ ├── revoke-share.command.ts -│ │ │ └── revoke-share.handler.ts -│ │ ├── queries/ -│ │ │ └── get-backup-share/ -│ │ │ ├── get-backup-share.query.ts -│ │ │ └── get-backup-share.handler.ts -│ │ ├── services/ -│ │ │ └── backup-share-application.service.ts -│ │ └── application.module.ts -│ │ -│ ├── domain/ # 领域层 (Core Business Logic) -│ │ ├── entities/ -│ │ │ └── backup-share.entity.ts -│ │ ├── repositories/ -│ │ │ └── backup-share.repository.interface.ts -│ │ ├── services/ -│ │ │ └── share-encryption.domain-service.ts -│ │ ├── value-objects/ -│ │ │ ├── share-id.vo.ts -│ │ │ └── encrypted-data.vo.ts -│ │ └── domain.module.ts -│ │ -│ ├── infrastructure/ # 基础设施层 (Adapters - Driven) -│ │ ├── persistence/ -│ │ │ ├── prisma/ -│ │ │ │ └── prisma.service.ts -│ │ │ └── repositories/ -│ │ │ └── backup-share.repository.impl.ts -│ │ ├── crypto/ -│ │ │ └── aes-encryption.service.ts -│ │ └── infrastructure.module.ts -│ │ -│ ├── shared/ # 共享模块 -│ │ ├── guards/ -│ │ │ └── service-auth.guard.ts # 服务间认证 -│ │ ├── filters/ -│ │ │ └── global-exception.filter.ts -│ │ └── interceptors/ -│ │ └── audit-log.interceptor.ts -│ │ -│ ├── config/ -│ │ └── index.ts -│ ├── app.module.ts -│ └── main.ts -│ -├── test/ -│ ├── unit/ -│ └── e2e/ -│ └── backup-share.e2e-spec.ts -│ -├── .env.example -├── .env.development -├── package.json -├── tsconfig.json -├── nest-cli.json -└── Dockerfile -``` - ---- - -## 3. 数据库设计 - -### 3.1 Prisma Schema - -```prisma -// prisma/schema.prisma - -generator client { - provider = "prisma-client-js" -} - -datasource db { - provider = "postgresql" - url = env("DATABASE_URL") -} - -// 备份分片存储 -model BackupShare { - shareId BigInt @id @default(autoincrement()) @map("share_id") - - // 用户标识 (来自 identity-service) - userId BigInt @unique @map("user_id") - accountSequence BigInt @unique @map("account_sequence") - - // MPC 密钥信息 - publicKey String @unique @map("public_key") @db.VarChar(130) - partyIndex Int @default(2) @map("party_index") // Backup = Party 2 - threshold Int @default(2) - totalParties Int @default(3) @map("total_parties") - - // 加密的分片数据 (AES-256-GCM 加密) - encryptedShareData String @map("encrypted_share_data") @db.Text - encryptionKeyId String @map("encryption_key_id") @db.VarChar(64) // 密钥轮换支持 - - // 状态管理 - status String @default("ACTIVE") @db.VarChar(20) // ACTIVE, REVOKED, ROTATED - - // 访问控制 - accessCount Int @default(0) @map("access_count") // 访问次数限制 - lastAccessedAt DateTime? @map("last_accessed_at") - - // 时间戳 - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime @updatedAt @map("updated_at") - revokedAt DateTime? @map("revoked_at") - - // 索引 - @@index([publicKey], name: "idx_backup_public_key") - @@index([status], name: "idx_backup_status") - @@index([createdAt], name: "idx_backup_created") - @@map("backup_shares") -} - -// 访问审计日志 -model ShareAccessLog { - logId BigInt @id @default(autoincrement()) @map("log_id") - - shareId BigInt @map("share_id") - userId BigInt @map("user_id") - - action String @db.VarChar(20) // STORE, RETRIEVE, REVOKE, ROTATE - sourceService String @map("source_service") @db.VarChar(50) // identity-service, recovery-service - sourceIp String @map("source_ip") @db.VarChar(45) - - success Boolean @default(true) - errorMessage String? @map("error_message") @db.Text - - createdAt DateTime @default(now()) @map("created_at") - - @@index([shareId], name: "idx_log_share") - @@index([userId], name: "idx_log_user") - @@index([action], name: "idx_log_action") - @@index([createdAt], name: "idx_log_created") - @@map("share_access_logs") -} -``` - ---- - -## 4. API 设计 - -### 4.1 存储备份分片 - -**POST /backup-share/store** - -由 identity-service 在创建账户时调用。 - -```typescript -// Request DTO -class StoreBackupShareDto { - @IsNotEmpty() - userId: string; // identity-service 的用户ID - - @IsNotEmpty() - accountSequence: number; // 账户序列号 - - @IsNotEmpty() - publicKey: string; // MPC 公钥 - - @IsNotEmpty() - encryptedShareData: string; // 已加密的分片数据 - - @IsOptional() - threshold?: number; // 默认 2 - - @IsOptional() - totalParties?: number; // 默认 3 -} - -// Response -{ - "success": true, - "shareId": "123", - "message": "Backup share stored successfully" -} -``` - -### 4.2 获取备份分片 - -**POST /backup-share/retrieve** - -用于账户恢复场景,需要额外的身份验证。 - -```typescript -// Request DTO -class RetrieveBackupShareDto { - @IsNotEmpty() - userId: string; - - @IsNotEmpty() - publicKey: string; - - @IsNotEmpty() - recoveryToken: string; // 恢复令牌 (由 identity-service 签发) - - @IsOptional() - deviceId?: string; // 新设备ID -} - -// Response -{ - "success": true, - "encryptedShareData": "...", - "partyIndex": 2, - "publicKey": "..." -} -``` - -### 4.3 撤销分片 - -**POST /backup-share/revoke** - -用于密钥轮换或账户注销。 - -```typescript -// Request DTO -class RevokeShareDto { - @IsNotEmpty() - userId: string; - - @IsNotEmpty() - publicKey: string; - - @IsNotEmpty() - reason: string; // ROTATION, ACCOUNT_CLOSED, SECURITY_BREACH -} -``` - ---- - -## 5. 服务间认证 - -### 5.1 认证机制 - -backup-service 不对外公开,只接受来自内部服务的请求。使用 **服务间 JWT** 或 **mTLS** 进行认证。 - -```typescript -// src/shared/guards/service-auth.guard.ts - -import { Injectable, CanActivate, ExecutionContext, UnauthorizedException } from '@nestjs/common'; -import { ConfigService } from '@nestjs/config'; -import * as jwt from 'jsonwebtoken'; - -@Injectable() -export class ServiceAuthGuard implements CanActivate { - constructor(private configService: ConfigService) {} - - canActivate(context: ExecutionContext): boolean { - const request = context.switchToHttp().getRequest(); - const serviceToken = request.headers['x-service-token']; - - if (!serviceToken) { - throw new UnauthorizedException('Missing service token'); - } - - try { - const secret = this.configService.get('SERVICE_JWT_SECRET'); - const payload = jwt.verify(serviceToken, secret) as { service: string }; - - // 只允许特定服务访问 - const allowedServices = ['identity-service', 'recovery-service']; - if (!allowedServices.includes(payload.service)) { - throw new UnauthorizedException('Service not authorized'); - } - - request.sourceService = payload.service; - return true; - } catch (error) { - throw new UnauthorizedException('Invalid service token'); - } - } -} -``` - -### 5.2 identity-service 调用示例 - -在 identity-service 中添加调用 backup-service 的客户端: - -```typescript -// identity-service/src/infrastructure/external/backup/backup-client.service.ts - -@Injectable() -export class BackupClientService { - private readonly backupServiceUrl: string; - private readonly serviceToken: string; - - constructor( - private readonly httpService: HttpService, - private readonly configService: ConfigService, - ) { - this.backupServiceUrl = this.configService.get('BACKUP_SERVICE_URL'); - this.serviceToken = this.generateServiceToken(); - } - - async storeBackupShare(params: { - userId: string; - accountSequence: number; - publicKey: string; - encryptedShareData: string; - }): Promise { - await firstValueFrom( - this.httpService.post( - `${this.backupServiceUrl}/backup-share/store`, - params, - { - headers: { - 'Content-Type': 'application/json', - 'X-Service-Token': this.serviceToken, - }, - }, - ), - ); - } - - private generateServiceToken(): string { - const secret = this.configService.get('SERVICE_JWT_SECRET'); - return jwt.sign( - { service: 'identity-service', iat: Math.floor(Date.now() / 1000) }, - secret, - { expiresIn: '1h' }, - ); - } -} -``` - ---- - -## 6. 核心实现 - -### 6.1 领域实体 - -```typescript -// src/domain/entities/backup-share.entity.ts - -export class BackupShare { - private constructor( - private readonly _shareId: bigint | null, - private readonly _userId: bigint, - private readonly _accountSequence: bigint, - private readonly _publicKey: string, - private readonly _partyIndex: number, - private readonly _threshold: number, - private readonly _totalParties: number, - private _encryptedShareData: string, - private _encryptionKeyId: string, - private _status: BackupShareStatus, - private _accessCount: number, - private readonly _createdAt: Date, - ) {} - - static create(params: { - userId: bigint; - accountSequence: bigint; - publicKey: string; - encryptedShareData: string; - encryptionKeyId: string; - }): BackupShare { - return new BackupShare( - null, - params.userId, - params.accountSequence, - params.publicKey, - 2, // Backup = Party 2 - 2, // threshold - 3, // totalParties - params.encryptedShareData, - params.encryptionKeyId, - BackupShareStatus.ACTIVE, - 0, - new Date(), - ); - } - - recordAccess(): void { - if (this._status !== BackupShareStatus.ACTIVE) { - throw new DomainError('Cannot access revoked share'); - } - this._accessCount++; - } - - revoke(reason: string): void { - if (this._status === BackupShareStatus.REVOKED) { - throw new DomainError('Share already revoked'); - } - this._status = BackupShareStatus.REVOKED; - } - - rotate(newEncryptedData: string, newKeyId: string): void { - this._encryptedShareData = newEncryptedData; - this._encryptionKeyId = newKeyId; - this._status = BackupShareStatus.ACTIVE; - } - - // Getters... - get shareId(): bigint | null { return this._shareId; } - get userId(): bigint { return this._userId; } - get publicKey(): string { return this._publicKey; } - get encryptedShareData(): string { return this._encryptedShareData; } - get status(): BackupShareStatus { return this._status; } - get accessCount(): number { return this._accessCount; } -} - -export enum BackupShareStatus { - ACTIVE = 'ACTIVE', - REVOKED = 'REVOKED', - ROTATED = 'ROTATED', -} -``` - -### 6.2 应用服务 - -```typescript -// src/application/services/backup-share-application.service.ts - -@Injectable() -export class BackupShareApplicationService { - private readonly logger = new Logger(BackupShareApplicationService.name); - - constructor( - @Inject(BACKUP_SHARE_REPOSITORY) - private readonly repository: BackupShareRepository, - private readonly encryptionService: AesEncryptionService, - private readonly auditService: AuditLogService, - ) {} - - async storeBackupShare(command: StoreBackupShareCommand): Promise { - this.logger.log(`Storing backup share for user: ${command.userId}`); - - // 检查是否已存在 - const existing = await this.repository.findByUserId(BigInt(command.userId)); - if (existing) { - throw new ApplicationError('Backup share already exists for this user'); - } - - // 二次加密 (identity-service 已加密一次,这里再加密一次) - const { encrypted, keyId } = await this.encryptionService.encrypt( - command.encryptedShareData, - ); - - // 创建实体 - const share = BackupShare.create({ - userId: BigInt(command.userId), - accountSequence: BigInt(command.accountSequence), - publicKey: command.publicKey, - encryptedShareData: encrypted, - encryptionKeyId: keyId, - }); - - // 保存 - const saved = await this.repository.save(share); - - // 记录审计日志 - await this.auditService.log({ - shareId: saved.shareId!, - userId: BigInt(command.userId), - action: 'STORE', - sourceService: command.sourceService, - sourceIp: command.sourceIp, - success: true, - }); - - this.logger.log(`Backup share stored: shareId=${saved.shareId}`); - return saved.shareId!.toString(); - } - - async retrieveBackupShare(query: GetBackupShareQuery): Promise { - this.logger.log(`Retrieving backup share for user: ${query.userId}`); - - const share = await this.repository.findByUserIdAndPublicKey( - BigInt(query.userId), - query.publicKey, - ); - - if (!share) { - throw new ApplicationError('Backup share not found'); - } - - if (share.status !== BackupShareStatus.ACTIVE) { - throw new ApplicationError('Backup share is not active'); - } - - // 记录访问 - share.recordAccess(); - await this.repository.save(share); - - // 解密 - const decrypted = await this.encryptionService.decrypt( - share.encryptedShareData, - share.encryptionKeyId, - ); - - // 记录审计日志 - await this.auditService.log({ - shareId: share.shareId!, - userId: BigInt(query.userId), - action: 'RETRIEVE', - sourceService: query.sourceService, - sourceIp: query.sourceIp, - success: true, - }); - - return { - encryptedShareData: decrypted, - partyIndex: share.partyIndex, - publicKey: share.publicKey, - }; - } -} -``` - ---- - -## 7. 环境配置 - -### 7.1 .env.example - -```bash -# Database (必须是独立的数据库实例,不能与 identity-service 共享!) -DATABASE_URL="postgresql://postgres:password@backup-db-server:5432/rwa_backup?schema=public" - -# Server -APP_PORT=3002 -APP_ENV="development" - -# Service Authentication -SERVICE_JWT_SECRET="your-super-secret-service-jwt-key" -ALLOWED_SERVICES="identity-service,recovery-service" - -# Encryption (用于二次加密备份分片) -BACKUP_ENCRYPTION_KEY="your-256-bit-encryption-key-in-hex" -BACKUP_ENCRYPTION_KEY_ID="key-v1" - -# Rate Limiting -MAX_RETRIEVE_PER_DAY=3 # 每用户每天最多获取 3 次 -MAX_STORE_PER_MINUTE=10 # 每分钟最多存储 10 个 - -# Audit -AUDIT_LOG_RETENTION_DAYS=365 # 审计日志保留 365 天 - -# Monitoring -PROMETHEUS_ENABLED=true -PROMETHEUS_PORT=9102 -``` - ---- - -## 8. 部署要求 - -### 8.1 物理隔离 - -**强制要求:** - -```yaml -# docker-compose.yml (仅供参考架构,生产环境必须分开部署) - -# ❌ 错误示例 - 同一台机器 -services: - identity-service: - ... - backup-service: # 不能在这里! - ... - -# ✅ 正确示例 - 分开部署 -# 机器 A: docker-compose-main.yml -services: - identity-service: - ... - -# 机器 B: docker-compose-backup.yml (异地机房) -services: - backup-service: - ... -``` - -### 8.2 网络安全 - -```yaml -# backup-service 的网络策略 -- 只允许来自 identity-service 的入站连接 -- 禁止任何公网访问 -- 使用 VPN 或专线连接主机房和备份机房 -``` - -### 8.3 数据库安全 - -``` -- 使用独立的 PostgreSQL 实例 -- 启用 TLS 加密连接 -- 定期备份到第三方存储 -- 启用审计日志 -``` - ---- - -## 9. 测试 - -### 9.1 E2E 测试 - -```typescript -// test/e2e/backup-share.e2e-spec.ts - -describe('BackupShare (e2e)', () => { - let app: INestApplication; - let serviceToken: string; - - beforeAll(async () => { - const moduleFixture = await Test.createTestingModule({ - imports: [AppModule], - }).compile(); - - app = moduleFixture.createNestApplication(); - await app.init(); - - // 生成测试用的服务令牌 - serviceToken = jwt.sign( - { service: 'identity-service' }, - process.env.SERVICE_JWT_SECRET, - ); - }); - - describe('POST /backup-share/store', () => { - it('should store backup share successfully', async () => { - const response = await request(app.getHttpServer()) - .post('/backup-share/store') - .set('X-Service-Token', serviceToken) - .send({ - userId: '12345', - accountSequence: 1001, - publicKey: '02' + 'a'.repeat(64), - encryptedShareData: 'encrypted-share-data-base64', - }) - .expect(201); - - expect(response.body.success).toBe(true); - expect(response.body.shareId).toBeDefined(); - }); - - it('should reject duplicate share', async () => { - // First store - await request(app.getHttpServer()) - .post('/backup-share/store') - .set('X-Service-Token', serviceToken) - .send({ - userId: '99999', - accountSequence: 9999, - publicKey: '02' + 'b'.repeat(64), - encryptedShareData: 'data', - }); - - // Duplicate - await request(app.getHttpServer()) - .post('/backup-share/store') - .set('X-Service-Token', serviceToken) - .send({ - userId: '99999', - accountSequence: 9999, - publicKey: '02' + 'b'.repeat(64), - encryptedShareData: 'data', - }) - .expect(400); - }); - - it('should reject unauthorized service', async () => { - await request(app.getHttpServer()) - .post('/backup-share/store') - .set('X-Service-Token', 'invalid-token') - .send({}) - .expect(401); - }); - }); - - describe('POST /backup-share/retrieve', () => { - it('should retrieve backup share with valid recovery token', async () => { - // Setup: store a share first - const storeResponse = await request(app.getHttpServer()) - .post('/backup-share/store') - .set('X-Service-Token', serviceToken) - .send({ - userId: '77777', - accountSequence: 7777, - publicKey: '02' + 'c'.repeat(64), - encryptedShareData: 'test-encrypted-data', - }); - - // Retrieve - const response = await request(app.getHttpServer()) - .post('/backup-share/retrieve') - .set('X-Service-Token', serviceToken) - .send({ - userId: '77777', - publicKey: '02' + 'c'.repeat(64), - recoveryToken: 'valid-recovery-token', - }) - .expect(200); - - expect(response.body.encryptedShareData).toBeDefined(); - expect(response.body.partyIndex).toBe(2); - }); - }); -}); -``` - ---- - -## 10. 与 identity-service 的集成 - -### 10.1 修改 identity-service - -在 identity-service 的 `autoCreateAccount` 中添加调用 backup-service: - -```typescript -// identity-service/src/application/services/user-application.service.ts - -// 在保存服务端分片后,调用 backup-service 存储备份分片 -await this.mpcKeyShareRepository.saveServerShare({...}); - -// 新增: 调用 backup-service 存储备份分片 -if (mpcResult.backupShareData) { - try { - await this.backupClient.storeBackupShare({ - userId: account.userId.toString(), - accountSequence: account.accountSequence.value, - publicKey: mpcResult.publicKey, - encryptedShareData: mpcResult.backupShareData, - }); - this.logger.log(`Backup share stored to backup-service for user: ${account.userId}`); - } catch (error) { - this.logger.error(`Failed to store backup share: ${error.message}`); - // 根据业务需求决定是否回滚或继续 - // 建议: 记录失败,后续通过补偿任务重试 - } -} -``` - -### 10.2 配置更新 - -在 identity-service 的 `.env` 中添加: - -```bash -# Backup Service -BACKUP_SERVICE_URL="http://backup-server:3002" -SERVICE_JWT_SECRET="your-super-secret-service-jwt-key" -``` - ---- - -## 11. 监控和告警 - -### 11.1 关键指标 - -``` -- backup_share_store_total # 存储总数 -- backup_share_retrieve_total # 获取总数 -- backup_share_revoke_total # 撤销总数 -- backup_share_store_latency_ms # 存储延迟 -- backup_share_retrieve_latency_ms # 获取延迟 -- backup_share_error_total # 错误总数 -``` - -### 11.2 告警规则 - -```yaml -# 异常访问告警 -- alert: HighBackupShareRetrieveRate - expr: rate(backup_share_retrieve_total[5m]) > 10 - for: 5m - labels: - severity: warning - annotations: - summary: "Backup share retrieve rate is high" - -# 服务不可用告警 -- alert: BackupServiceDown - expr: up{job="backup-service"} == 0 - for: 1m - labels: - severity: critical -``` - ---- - -## 12. 开发步骤 - -### 12.1 初始化项目 - -```bash -# 1. 创建 NestJS 项目 -cd backend/services/backup-service -npx @nestjs/cli new . --skip-git - -# 2. 安装依赖 -npm install @nestjs/config @prisma/client class-validator class-transformer -npm install -D prisma - -# 3. 初始化 Prisma -npx prisma init - -# 4. 复制 schema 并生成客户端 -npx prisma generate -npx prisma migrate dev --name init -``` - -### 12.2 开发顺序 - -1. **基础设施层** - Prisma 配置、数据库连接 -2. **领域层** - BackupShare 实体、Repository 接口 -3. **应用层** - 存储/获取/撤销命令处理 -4. **接口层** - REST API Controller -5. **安全层** - ServiceAuthGuard、审计日志 -6. **测试** - 单元测试、E2E 测试 - ---- - -## 13. 检查清单 - -开发完成后,确保以下事项: - -- [ ] 数据库部署在独立服务器 -- [ ] 服务间认证已实现 -- [ ] 分片数据已二次加密 -- [ ] 审计日志已启用 -- [ ] 访问频率限制已实现 -- [ ] E2E 测试通过 -- [ ] 与 identity-service 集成测试通过 -- [ ] 监控指标已配置 -- [ ] 告警规则已配置 -- [ ] 生产环境部署在异地机房 diff --git a/backend/services/backup-service/README.md b/backend/services/backup-service/README.md new file mode 100644 index 00000000..8f0f65f7 --- /dev/null +++ b/backend/services/backup-service/README.md @@ -0,0 +1,98 @@ +

+ Nest Logo +

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

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

+

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

+ + +## Description + +[Nest](https://github.com/nestjs/nest) framework TypeScript starter repository. + +## Project setup + +```bash +$ npm install +``` + +## Compile and run the project + +```bash +# development +$ npm run start + +# watch mode +$ npm run start:dev + +# production mode +$ npm run start:prod +``` + +## Run tests + +```bash +# unit tests +$ npm run test + +# e2e tests +$ npm run test:e2e + +# test coverage +$ npm run test:cov +``` + +## Deployment + +When you're ready to deploy your NestJS application to production, there are some key steps you can take to ensure it runs as efficiently as possible. Check out the [deployment documentation](https://docs.nestjs.com/deployment) for more information. + +If you are looking for a cloud-based platform to deploy your NestJS application, check out [Mau](https://mau.nestjs.com), our official platform for deploying NestJS applications on AWS. Mau makes deployment straightforward and fast, requiring just a few simple steps: + +```bash +$ npm install -g @nestjs/mau +$ mau deploy +``` + +With Mau, you can deploy your application in just a few clicks, allowing you to focus on building features rather than managing infrastructure. + +## Resources + +Check out a few resources that may come in handy when working with NestJS: + +- Visit the [NestJS Documentation](https://docs.nestjs.com) to learn more about the framework. +- For questions and support, please visit our [Discord channel](https://discord.gg/G7Qnnhy). +- To dive deeper and get more hands-on experience, check out our official video [courses](https://courses.nestjs.com/). +- Deploy your application to AWS with the help of [NestJS Mau](https://mau.nestjs.com) in just a few clicks. +- Visualize your application graph and interact with the NestJS application in real-time using [NestJS Devtools](https://devtools.nestjs.com). +- Need help with your project (part-time to full-time)? Check out our official [enterprise support](https://enterprise.nestjs.com). +- To stay in the loop and get updates, follow us on [X](https://x.com/nestframework) and [LinkedIn](https://linkedin.com/company/nestjs). +- Looking for a job, or have a job to offer? Check out our official [Jobs board](https://jobs.nestjs.com). + +## Support + +Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support). + +## Stay in touch + +- Author - [Kamil Myśliwiec](https://twitter.com/kammysliwiec) +- Website - [https://nestjs.com](https://nestjs.com/) +- Twitter - [@nestframework](https://twitter.com/nestframework) + +## License + +Nest is [MIT licensed](https://github.com/nestjs/nest/blob/master/LICENSE). diff --git a/backend/services/backup-service/docker-compose.test.yml b/backend/services/backup-service/docker-compose.test.yml new file mode 100644 index 00000000..5550a49d --- /dev/null +++ b/backend/services/backup-service/docker-compose.test.yml @@ -0,0 +1,25 @@ +version: '3.8' + +services: + test-db: + image: postgres:15-alpine + container_name: backup-service-test-db + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: testpassword + POSTGRES_DB: rwa_backup_test + ports: + - "5434:5432" + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres"] + interval: 5s + timeout: 5s + retries: 10 + tmpfs: + - /var/lib/postgresql/data + command: > + postgres + -c fsync=off + -c synchronous_commit=off + -c full_page_writes=off + -c random_page_cost=1.0 diff --git a/backend/services/backup-service/docker-compose.yml b/backend/services/backup-service/docker-compose.yml new file mode 100644 index 00000000..c0a9fde6 --- /dev/null +++ b/backend/services/backup-service/docker-compose.yml @@ -0,0 +1,53 @@ +version: '3.8' + +services: + backup-service: + build: + context: . + dockerfile: Dockerfile + container_name: backup-service + ports: + - "${APP_PORT:-3002}:3002" + environment: + - DATABASE_URL=postgresql://postgres:password@backup-db:5432/rwa_backup?schema=public + - APP_PORT=3002 + - APP_ENV=development + - SERVICE_JWT_SECRET=${SERVICE_JWT_SECRET} + - ALLOWED_SERVICES=identity-service,recovery-service + - BACKUP_ENCRYPTION_KEY=${BACKUP_ENCRYPTION_KEY} + - BACKUP_ENCRYPTION_KEY_ID=${BACKUP_ENCRYPTION_KEY_ID:-key-v1} + - MAX_RETRIEVE_PER_DAY=3 + - MAX_STORE_PER_MINUTE=10 + depends_on: + backup-db: + condition: service_healthy + networks: + - backup-network + restart: unless-stopped + + backup-db: + image: postgres:15-alpine + container_name: backup-db + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: password + POSTGRES_DB: rwa_backup + volumes: + - backup-db-data:/var/lib/postgresql/data + ports: + - "5433:5432" # Different port to avoid conflict with main db + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres"] + interval: 10s + timeout: 5s + retries: 5 + networks: + - backup-network + restart: unless-stopped + +volumes: + backup-db-data: + +networks: + backup-network: + driver: bridge diff --git a/backend/services/backup-service/docs/API.md b/backend/services/backup-service/docs/API.md new file mode 100644 index 00000000..c70ee566 --- /dev/null +++ b/backend/services/backup-service/docs/API.md @@ -0,0 +1,613 @@ +# Backup Service API Reference + +## Overview + +The backup-service exposes RESTful APIs for managing MPC backup shares. All endpoints (except health checks) require service-to-service JWT authentication. + +**Base URL:** `http://localhost:3002` + +--- + +## Authentication + +All `/backup-share/*` endpoints require a service JWT token in the `X-Service-Token` header. + +### Token Format + +```json +{ + "service": "identity-service", + "iat": 1704067200, + "exp": 1704153600 +} +``` + +### Generating Service Tokens + +```typescript +import jwt from 'jsonwebtoken'; + +const token = jwt.sign( + { service: 'identity-service' }, + process.env.SERVICE_JWT_SECRET, + { expiresIn: '24h' } +); +``` + +### Allowed Services + +Only the following services can access the backup-service: +- `identity-service` - Primary service for MPC operations +- `recovery-service` - Account recovery operations + +Configure via `ALLOWED_SERVICES` environment variable. + +--- + +## API Endpoints + +### 1. Store Backup Share + +Store an encrypted MPC backup share for a user. + +**Endpoint:** `POST /backup-share/store` + +**Headers:** +| Header | Required | Description | +|--------|----------|-------------| +| `X-Service-Token` | Yes | Service JWT token | +| `Content-Type` | Yes | `application/json` | + +**Request Body:** + +```json +{ + "userId": "12345", + "accountSequence": 1001, + "publicKey": "02aabbccdd...", + "encryptedShareData": "base64-encoded-encrypted-data", + "threshold": 2, + "totalParties": 3 +} +``` + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `userId` | string | Yes | User identifier from identity-service | +| `accountSequence` | number | Yes | Account sequence number (min: 1) | +| `publicKey` | string | Yes | MPC public key (66-130 characters) | +| `encryptedShareData` | string | Yes | Pre-encrypted share data (AES-256-GCM by identity-service) | +| `threshold` | number | No | Reconstruction threshold (default: 2, range: 2-10) | +| `totalParties` | number | No | Total MPC parties (default: 3, range: 2-10) | + +**Success Response (201 Created):** + +```json +{ + "success": true, + "shareId": "1", + "message": "Backup share stored successfully" +} +``` + +**Error Responses:** + +| Status | Code | Description | +|--------|------|-------------| +| 400 | `VALIDATION_ERROR` | Invalid request body | +| 401 | `UNAUTHORIZED` | Missing or invalid service token | +| 409 | `SHARE_ALREADY_EXISTS` | Share already exists for this user | + +**Example:** + +```bash +curl -X POST http://localhost:3002/backup-share/store \ + -H "X-Service-Token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." \ + -H "Content-Type: application/json" \ + -d '{ + "userId": "12345", + "accountSequence": 1001, + "publicKey": "02aabbccddee...", + "encryptedShareData": "encrypted-share-data-here" + }' +``` + +--- + +### 2. Retrieve Backup Share + +Retrieve an encrypted MPC backup share for account recovery. + +**Endpoint:** `POST /backup-share/retrieve` + +**Headers:** +| Header | Required | Description | +|--------|----------|-------------| +| `X-Service-Token` | Yes | Service JWT token | +| `Content-Type` | Yes | `application/json` | + +**Request Body:** + +```json +{ + "userId": "12345", + "publicKey": "02aabbccdd...", + "recoveryToken": "valid-recovery-token", + "deviceId": "device-uuid" +} +``` + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `userId` | string | Yes | User identifier | +| `publicKey` | string | Yes | MPC public key (66-130 characters) | +| `recoveryToken` | string | Yes | Recovery token verified by identity-service | +| `deviceId` | string | No | Device identifier for audit logging | + +**Success Response (200 OK):** + +```json +{ + "success": true, + "encryptedShareData": "base64-encoded-encrypted-data", + "partyIndex": 2, + "publicKey": "02aabbccdd..." +} +``` + +| Field | Type | Description | +|-------|------|-------------| +| `success` | boolean | Operation success status | +| `encryptedShareData` | string | Decrypted share data (still encrypted by identity-service) | +| `partyIndex` | number | Party index (always 2 for backup share) | +| `publicKey` | string | MPC public key | + +**Error Responses:** + +| Status | Code | Description | +|--------|------|-------------| +| 400 | `SHARE_NOT_ACTIVE` | Share has been revoked | +| 401 | `UNAUTHORIZED` | Missing or invalid service token | +| 404 | `SHARE_NOT_FOUND` | No share found for user/publicKey | +| 429 | `RATE_LIMIT_EXCEEDED` | Max 3 retrievals per day exceeded | + +**Rate Limiting:** +- Maximum 3 retrieves per user per day +- Configurable via `MAX_RETRIEVE_PER_DAY` environment variable +- Counter resets at midnight UTC + +**Example:** + +```bash +curl -X POST http://localhost:3002/backup-share/retrieve \ + -H "X-Service-Token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." \ + -H "Content-Type: application/json" \ + -d '{ + "userId": "12345", + "publicKey": "02aabbccddee...", + "recoveryToken": "valid-token-from-identity-service" + }' +``` + +--- + +### 3. Revoke Backup Share + +Revoke an existing backup share (for key rotation or account closure). + +**Endpoint:** `POST /backup-share/revoke` + +**Headers:** +| Header | Required | Description | +|--------|----------|-------------| +| `X-Service-Token` | Yes | Service JWT token | +| `Content-Type` | Yes | `application/json` | + +**Request Body:** + +```json +{ + "userId": "12345", + "publicKey": "02aabbccdd...", + "reason": "ROTATION" +} +``` + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `userId` | string | Yes | User identifier | +| `publicKey` | string | Yes | MPC public key (66-130 characters) | +| `reason` | string | Yes | Revocation reason (see below) | + +**Valid Revocation Reasons:** + +| Reason | Description | +|--------|-------------| +| `ROTATION` | Key rotation (new share will be stored) | +| `ACCOUNT_CLOSED` | User account permanently closed | +| `SECURITY_BREACH` | Security incident requiring key invalidation | +| `USER_REQUEST` | User requested share removal | + +**Success Response (200 OK):** + +```json +{ + "success": true, + "message": "Backup share revoked successfully" +} +``` + +**Error Responses:** + +| Status | Code | Description | +|--------|------|-------------| +| 400 | `VALIDATION_ERROR` | Invalid reason value | +| 401 | `UNAUTHORIZED` | Missing or invalid service token | +| 404 | `SHARE_NOT_FOUND` | No share found for user/publicKey | + +**Example:** + +```bash +curl -X POST http://localhost:3002/backup-share/revoke \ + -H "X-Service-Token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." \ + -H "Content-Type: application/json" \ + -d '{ + "userId": "12345", + "publicKey": "02aabbccddee...", + "reason": "ROTATION" + }' +``` + +--- + +### 4. Health Check + +Basic health check endpoint (no authentication required). + +**Endpoint:** `GET /health` + +**Response (200 OK):** + +```json +{ + "status": "ok", + "timestamp": "2025-01-15T10:30:45.123Z", + "service": "backup-service" +} +``` + +--- + +### 5. Readiness Probe + +Kubernetes readiness probe with database connectivity check. + +**Endpoint:** `GET /health/ready` + +**Success Response (200 OK):** + +```json +{ + "status": "ready", + "database": "connected", + "timestamp": "2025-01-15T10:30:45.123Z" +} +``` + +**Failure Response (503 Service Unavailable):** + +```json +{ + "status": "not ready", + "database": "disconnected", + "error": "Connection refused", + "timestamp": "2025-01-15T10:30:45.123Z" +} +``` + +--- + +### 6. Liveness Probe + +Kubernetes liveness probe. + +**Endpoint:** `GET /health/live` + +**Response (200 OK):** + +```json +{ + "status": "alive", + "timestamp": "2025-01-15T10:30:45.123Z" +} +``` + +--- + +## Error Response Format + +All error responses follow this standardized format: + +```json +{ + "success": false, + "error": "Human-readable error message", + "code": "MACHINE_READABLE_CODE", + "timestamp": "2025-01-15T10:30:45.123Z", + "path": "/backup-share/store" +} +``` + +### Error Codes Reference + +| Code | HTTP Status | Description | +|------|-------------|-------------| +| `VALIDATION_ERROR` | 400 | Request validation failed | +| `SHARE_NOT_ACTIVE` | 400 | Share has been revoked | +| `UNAUTHORIZED` | 401 | Authentication failed | +| `FORBIDDEN` | 403 | Service not authorized | +| `SHARE_NOT_FOUND` | 404 | Share not found | +| `SHARE_ALREADY_EXISTS` | 409 | Duplicate share | +| `RATE_LIMIT_EXCEEDED` | 429 | Rate limit exceeded | +| `INTERNAL_ERROR` | 500 | Internal server error | + +--- + +## Data Validation Rules + +### Public Key + +- Length: 66-130 characters +- Format: Hexadecimal string +- 66 chars = compressed ECDSA public key (02/03 prefix + 64 hex chars) +- 130 chars = uncompressed ECDSA public key (04 prefix + 128 hex chars) + +### User ID + +- Type: String (numeric format) +- Required: Yes +- Pattern: Positive integer as string + +### Account Sequence + +- Type: Number +- Minimum: 1 +- Required: Yes + +### Encrypted Share Data + +- Type: String +- Required: Yes +- Format: Base64-encoded encrypted data (from identity-service) + +--- + +## Workflow Examples + +### Complete Account Setup Flow + +``` +1. User creates account on identity-service +2. identity-service generates MPC key shares (3 parties) +3. identity-service stores Party 0 (Server Share) locally +4. identity-service sends Party 1 (Client Share) to user device +5. identity-service calls backup-service to store Party 2 (Backup Share) + +POST /backup-share/store +{ + "userId": "12345", + "accountSequence": 1001, + "publicKey": "02...", + "encryptedShareData": "encrypted-party-2-data" +} +``` + +### Account Recovery Flow + +``` +1. User loses device and initiates recovery +2. User verifies identity through identity-service +3. identity-service generates recovery token +4. identity-service retrieves backup share + +POST /backup-share/retrieve +{ + "userId": "12345", + "publicKey": "02...", + "recoveryToken": "valid-recovery-token" +} + +5. identity-service combines Party 0 + Party 2 to reconstruct key +6. New Party 1 share generated for new device +``` + +### Key Rotation Flow + +``` +1. Security event triggers key rotation +2. New key shares generated +3. Old backup share revoked + +POST /backup-share/revoke +{ + "userId": "12345", + "publicKey": "02...", + "reason": "ROTATION" +} + +4. New backup share stored + +POST /backup-share/store +{ + "userId": "12345", + "accountSequence": 1002, + "publicKey": "03...", + "encryptedShareData": "new-encrypted-data" +} +``` + +--- + +## Security Considerations + +### Double Encryption + +Data is encrypted twice: +1. **First layer:** identity-service encrypts share data before sending +2. **Second layer:** backup-service encrypts again using AES-256-GCM + +This ensures data remains protected even if one system is compromised. + +### Rate Limiting + +- Retrieves are limited to prevent brute-force attacks +- Default: 3 retrieves per user per day +- Configurable via environment variable + +### Audit Logging + +All operations are logged with: +- Timestamp +- User ID +- Action (STORE/RETRIEVE/REVOKE) +- Source service +- Source IP address +- Success/failure status + +### Sensitive Data Handling + +The following fields are redacted in logs: +- `encryptedShareData` +- `recoveryToken` +- `password` +- `secret` + +--- + +## SDK Examples + +### TypeScript/JavaScript + +```typescript +import axios from 'axios'; +import jwt from 'jsonwebtoken'; + +const BACKUP_SERVICE_URL = 'http://localhost:3002'; +const SERVICE_JWT_SECRET = process.env.SERVICE_JWT_SECRET; + +function generateServiceToken(): string { + return jwt.sign( + { service: 'identity-service' }, + SERVICE_JWT_SECRET, + { expiresIn: '1h' } + ); +} + +async function storeBackupShare( + userId: string, + accountSequence: number, + publicKey: string, + encryptedShareData: string +): Promise { + const response = await axios.post( + `${BACKUP_SERVICE_URL}/backup-share/store`, + { + userId, + accountSequence, + publicKey, + encryptedShareData, + }, + { + headers: { + 'X-Service-Token': generateServiceToken(), + 'Content-Type': 'application/json', + }, + } + ); + return response.data.shareId; +} + +async function retrieveBackupShare( + userId: string, + publicKey: string, + recoveryToken: string +): Promise<{ encryptedShareData: string; partyIndex: number }> { + const response = await axios.post( + `${BACKUP_SERVICE_URL}/backup-share/retrieve`, + { + userId, + publicKey, + recoveryToken, + }, + { + headers: { + 'X-Service-Token': generateServiceToken(), + 'Content-Type': 'application/json', + }, + } + ); + return { + encryptedShareData: response.data.encryptedShareData, + partyIndex: response.data.partyIndex, + }; +} + +async function revokeBackupShare( + userId: string, + publicKey: string, + reason: 'ROTATION' | 'ACCOUNT_CLOSED' | 'SECURITY_BREACH' | 'USER_REQUEST' +): Promise { + await axios.post( + `${BACKUP_SERVICE_URL}/backup-share/revoke`, + { + userId, + publicKey, + reason, + }, + { + headers: { + 'X-Service-Token': generateServiceToken(), + 'Content-Type': 'application/json', + }, + } + ); +} +``` + +### cURL Examples + +```bash +# Store a backup share +curl -X POST http://localhost:3002/backup-share/store \ + -H "X-Service-Token: $SERVICE_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "userId": "12345", + "accountSequence": 1001, + "publicKey": "02aabbccddee1122334455667788990011223344556677889900112233445566", + "encryptedShareData": "encrypted-data-here" + }' + +# Retrieve a backup share +curl -X POST http://localhost:3002/backup-share/retrieve \ + -H "X-Service-Token: $SERVICE_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "userId": "12345", + "publicKey": "02aabbccddee1122334455667788990011223344556677889900112233445566", + "recoveryToken": "recovery-token-here" + }' + +# Revoke a backup share +curl -X POST http://localhost:3002/backup-share/revoke \ + -H "X-Service-Token: $SERVICE_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "userId": "12345", + "publicKey": "02aabbccddee1122334455667788990011223344556677889900112233445566", + "reason": "ROTATION" + }' + +# Health check +curl http://localhost:3002/health +``` diff --git a/backend/services/backup-service/docs/ARCHITECTURE.md b/backend/services/backup-service/docs/ARCHITECTURE.md new file mode 100644 index 00000000..2d7f79ab --- /dev/null +++ b/backend/services/backup-service/docs/ARCHITECTURE.md @@ -0,0 +1,430 @@ +# Backup Service Architecture + +## Overview + +**Service Name:** `backup-service` +**Version:** 1.0.0 +**Description:** RWA Durian MPC Backup Share Storage Service +**Primary Purpose:** Securely store and manage MPC backup shares (Party 2/3) for account recovery + +## Core Responsibilities + +- Store encrypted MPC backup share data (Party 2) +- Provide share retrieval for account recovery scenarios +- Support share revocation for key rotation or account closure +- Maintain comprehensive audit logs for all operations +- Implement rate limiting and access controls + +## Critical Security Requirement + +**Physical server isolation from identity-service is MANDATORY.** The backup-service must be deployed on a physically separate server to maintain MPC security. If compromised alone, attackers can only obtain 1 of 3 shares, making key reconstruction impossible. + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ MPC Key Distribution │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ Party 0 (Server Share) Party 1 (Client Share) Party 2 (Backup) │ +│ ┌─────────────────┐ ┌─────────────────┐ ┌──────────────┐ │ +│ │ identity-service│ │ User Device │ │backup-service│ │ +│ │ (Server A) │ │ (Mobile/Web) │ │ (Server B) │ │ +│ └─────────────────┘ └─────────────────┘ └──────────────┘ │ +│ │ +│ 2-of-3 Threshold: Any 2 shares can reconstruct the private key │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## DDD + Hexagonal Architecture + +The service follows a layered architecture with clear separation of concerns: + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ API Layer (Adapters) │ +│ Controllers, DTOs, HTTP Request/Response Handling │ +├─────────────────────────────────────────────────────────────────────────┤ +│ Application Layer │ +│ Use Cases, Commands, Queries, Handlers, Services │ +├─────────────────────────────────────────────────────────────────────────┤ +│ Domain Layer │ +│ Entities, Value Objects, Repositories (Interfaces), Business Logic │ +├─────────────────────────────────────────────────────────────────────────┤ +│ Infrastructure Layer (Adapters) │ +│ Persistence (Prisma/PostgreSQL), Encryption, Crypto │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +### Layer Dependencies (Dependency Rule) + +``` +API Layer ──────────────▶ Application Layer + │ + ▼ + Domain Layer ◀─────── Infrastructure Layer + │ │ + ▼ ▼ + (Interfaces defined) (Implementations) +``` + +**Key Principle:** Dependencies point inward. Domain layer has no external dependencies. + +--- + +## Design Patterns + +| Pattern | Implementation | Files | +|---------|----------------|-------| +| **Command Pattern** | Store, Revoke operations | `store-backup-share.command.ts`, `revoke-share.command.ts` | +| **Query Pattern** | Retrieve operation | `get-backup-share.query.ts` | +| **Repository Pattern** | Data access abstraction | `backup-share.repository.interface.ts`, `backup-share.repository.impl.ts` | +| **Dependency Injection** | NestJS DI Container | `*.module.ts` | +| **Guard Pattern** | Authentication & Authorization | `service-auth.guard.ts` | +| **Filter Pattern** | Global exception handling | `global-exception.filter.ts` | +| **Interceptor Pattern** | Request/response processing | `audit-log.interceptor.ts` | +| **Value Objects** | Immutable domain concepts | `share-id.vo.ts`, `encrypted-data.vo.ts` | + +--- + +## Directory Structure + +``` +backup-service/ +├── prisma/ +│ ├── schema.prisma # Database schema definition +│ └── migrations/ # Database migration history +│ +├── src/ +│ ├── api/ # Adapter Layer (External Interface) +│ │ ├── controllers/ +│ │ │ ├── backup-share.controller.ts # Main API endpoints +│ │ │ └── health.controller.ts # Health check endpoints +│ │ ├── dto/ +│ │ │ ├── request/ +│ │ │ │ ├── store-share.dto.ts # Store share request +│ │ │ │ ├── retrieve-share.dto.ts # Retrieve share request +│ │ │ │ └── revoke-share.dto.ts # Revoke share request +│ │ │ └── response/ +│ │ │ └── share-info.dto.ts # Response DTOs +│ │ └── api.module.ts +│ │ +│ ├── application/ # Use Cases Layer +│ │ ├── commands/ +│ │ │ ├── store-backup-share/ +│ │ │ │ ├── store-backup-share.command.ts +│ │ │ │ └── store-backup-share.handler.ts +│ │ │ └── revoke-share/ +│ │ │ ├── revoke-share.command.ts +│ │ │ └── revoke-share.handler.ts +│ │ ├── queries/ +│ │ │ └── get-backup-share/ +│ │ │ ├── get-backup-share.query.ts +│ │ │ └── get-backup-share.handler.ts +│ │ ├── services/ +│ │ │ └── backup-share-application.service.ts +│ │ ├── errors/ +│ │ │ └── application.error.ts +│ │ └── application.module.ts +│ │ +│ ├── domain/ # Core Business Logic +│ │ ├── entities/ +│ │ │ └── backup-share.entity.ts # BackupShare aggregate root +│ │ ├── repositories/ +│ │ │ └── backup-share.repository.interface.ts +│ │ ├── value-objects/ +│ │ │ ├── share-id.vo.ts # Immutable share identifier +│ │ │ └── encrypted-data.vo.ts # Encrypted data structure +│ │ ├── errors/ +│ │ │ └── domain.error.ts +│ │ └── domain.module.ts +│ │ +│ ├── infrastructure/ # Adapter Layer (Services) +│ │ ├── persistence/ +│ │ │ ├── prisma/ +│ │ │ │ └── prisma.service.ts # Prisma ORM service +│ │ │ └── repositories/ +│ │ │ ├── backup-share.repository.impl.ts # Repository implementation +│ │ │ └── audit-log.repository.ts # Audit logging +│ │ ├── crypto/ +│ │ │ └── aes-encryption.service.ts # AES-256-GCM encryption +│ │ └── infrastructure.module.ts +│ │ +│ ├── shared/ # Cross-cutting Concerns +│ │ ├── guards/ +│ │ │ └── service-auth.guard.ts # JWT service authentication +│ │ ├── filters/ +│ │ │ └── global-exception.filter.ts # Exception handling +│ │ └── interceptors/ +│ │ └── audit-log.interceptor.ts # Request/response logging +│ │ +│ ├── config/ +│ │ └── index.ts # Centralized configuration +│ ├── app.module.ts # Root NestJS module +│ └── main.ts # Application entry point +│ +├── test/ # Test files +│ ├── unit/ +│ ├── integration/ +│ ├── e2e/ +│ ├── setup/ +│ └── utils/ +│ +└── docs/ # Documentation +``` + +--- + +## Domain Layer Details + +### BackupShare Entity (Aggregate Root) + +**File:** `src/domain/entities/backup-share.entity.ts` + +```typescript +class BackupShare { + // Identity + shareId: bigint | null // Auto-increment primary key + userId: bigint // From identity-service + accountSequence: bigint // Account identifier + + // MPC Configuration + publicKey: string // MPC public key (66-130 chars) + partyIndex: number // Always 2 for backup share + threshold: number // Default 2 (for 2-of-3 scheme) + totalParties: number // Default 3 + + // Encrypted Data + encryptedShareData: string // AES-256-GCM encrypted data + encryptionKeyId: string // For key rotation support + + // State Management + status: BackupShareStatus // ACTIVE | REVOKED | ROTATED + accessCount: number // Track access frequency + lastAccessedAt: Date | null + + // Timestamps + createdAt: Date + updatedAt: Date + revokedAt: Date | null +} + +// Factory Methods +BackupShare.create(params): BackupShare +BackupShare.reconstitute(props): BackupShare + +// Domain Methods +recordAccess(): void // Increment access counter +revoke(reason: string): void // Mark as revoked +rotate(newData, newKeyId): void // Key rotation support +isActive(): boolean +``` + +### Value Objects + +#### ShareId +```typescript +class ShareId { + static create(value: bigint | string | number): ShareId + get value(): bigint + toString(): string + equals(other: ShareId): boolean +} +``` + +#### EncryptedData +```typescript +class EncryptedData { + ciphertext: string // Base64 encoded + iv: string // Base64 encoded + authTag: string // Base64 encoded + keyId: string + + static create(params): EncryptedData + static fromSerializedString(serialized, keyId): EncryptedData + toSerializedString(): string +} +``` + +### Repository Interface + +```typescript +interface BackupShareRepository { + save(share: BackupShare): Promise + findById(shareId: bigint): Promise + findByUserId(userId: bigint): Promise + findByPublicKey(publicKey: string): Promise + findByUserIdAndPublicKey(userId: bigint, publicKey: string): Promise + findByAccountSequence(accountSequence: bigint): Promise + delete(shareId: bigint): Promise +} +``` + +--- + +## Application Layer Details + +### Command Handlers + +#### StoreBackupShareHandler + +**Flow:** +1. Check if share already exists for user (uniqueness constraint) +2. Check if share already exists for public key (uniqueness constraint) +3. Apply double encryption (AES-256-GCM) +4. Create BackupShare domain entity +5. Save to repository +6. Log audit event +7. Return shareId + +#### RevokeShareHandler + +**Flow:** +1. Find share by userId and publicKey +2. Call entity's `revoke()` method +3. Save changes to repository +4. Log audit event + +### Query Handlers + +#### GetBackupShareHandler + +**Flow:** +1. Check rate limit (max 3 retrieves per day per user) +2. Find share by userId and publicKey +3. Verify share is ACTIVE +4. Record access in entity +5. Save entity state +6. Decrypt share data (removes our encryption layer) +7. Log audit event +8. Return decrypted data + +--- + +## Infrastructure Layer Details + +### Encryption Service + +**Algorithm:** AES-256-GCM (authenticated encryption) +**IV Length:** 12 bytes (96 bits) +**Key Size:** 32 bytes (256 bits) +**Output Format:** `{ciphertext}:{iv}:{authTag}` (colon-separated base64) + +```typescript +class AesEncryptionService { + async encrypt(plaintext: string): Promise + async decrypt(encryptedData: string, keyId: string): Promise + addKey(keyId: string, keyHex: string): void + getCurrentKeyId(): string +} +``` + +### Prisma ORM Service + +Uses `@prisma/adapter-pg` for Prisma 7.x compatibility with PostgreSQL. + +```typescript +class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy { + async onModuleInit() // Connect on startup + async onModuleDestroy() // Disconnect on shutdown + async cleanDatabase() // Test utility - delete all tables +} +``` + +--- + +## Database Schema + +### BackupShare Table + +| Column | Type | Constraints | Description | +|--------|------|-------------|-------------| +| share_id | BIGSERIAL | PK | Auto-increment ID | +| user_id | BIGINT | UNIQUE, NOT NULL | User identifier | +| account_sequence | BIGINT | UNIQUE, NOT NULL | Account sequence | +| public_key | VARCHAR(130) | UNIQUE, NOT NULL | MPC public key | +| party_index | INT | DEFAULT 2 | Party index (always 2) | +| threshold | INT | DEFAULT 2 | Threshold for reconstruction | +| total_parties | INT | DEFAULT 3 | Total parties | +| encrypted_share_data | TEXT | NOT NULL | Encrypted share data | +| encryption_key_id | VARCHAR(64) | NOT NULL | Encryption key ID | +| status | VARCHAR(20) | DEFAULT 'ACTIVE' | Share status | +| access_count | INT | DEFAULT 0 | Access counter | +| last_accessed_at | TIMESTAMP | NULLABLE | Last access time | +| created_at | TIMESTAMP | DEFAULT NOW() | Creation time | +| updated_at | TIMESTAMP | AUTO | Update time | +| revoked_at | TIMESTAMP | NULLABLE | Revocation time | + +### ShareAccessLog Table + +| Column | Type | Constraints | Description | +|--------|------|-------------|-------------| +| log_id | BIGSERIAL | PK | Auto-increment ID | +| share_id | BIGINT | NOT NULL | Reference to share | +| user_id | BIGINT | NOT NULL | User identifier | +| action | VARCHAR(20) | NOT NULL | STORE/RETRIEVE/REVOKE/ROTATE | +| source_service | VARCHAR(50) | NOT NULL | Calling service | +| source_ip | VARCHAR(45) | NOT NULL | Client IP | +| success | BOOLEAN | DEFAULT TRUE | Operation success | +| error_message | TEXT | NULLABLE | Error details | +| created_at | TIMESTAMP | DEFAULT NOW() | Log time | + +--- + +## Key Architectural Decisions + +### 1. Double Encryption + +- Identity-service encrypts data once +- Backup-service encrypts again (AES-256-GCM) +- Defense-in-depth: even if one system is compromised, data remains encrypted + +### 2. Physical Server Isolation + +- MPC scheme is 2-of-3: requires at least 2 shares to reconstruct key +- Party 0 (Server Share) on identity-service +- Party 2 (Backup Share) on separate backup-service +- Party 1 (Client Share) on user device +- If only one server is compromised, MPC security remains intact + +### 3. Audit Logging + +- All operations logged with timestamp, user, action, source service, source IP +- Non-blocking writes (errors don't affect main operations) +- Supports compliance and security investigations + +### 4. Rate Limiting + +- Max 3 retrieves per user per day (prevents brute force recovery attempts) +- Configurable via `MAX_RETRIEVE_PER_DAY` +- Tracked in database, can be monitored for anomalies + +### 5. Service-to-Service Auth + +- JWT tokens with service identity +- No user authentication on backup-service (identity-service responsible) +- Simplified client trust model: only trust from known services + +### 6. Error Handling + +- Structured error codes for programmatic handling +- Sensitive data redacted from logs +- Standard error response format + +--- + +## Key Files Reference + +| File Path | Purpose | +|-----------|---------| +| `src/main.ts` | Entry point, NestFactory bootstrap | +| `src/app.module.ts` | Root module, global filters/interceptors | +| `src/config/index.ts` | Centralized configuration | +| `src/domain/entities/backup-share.entity.ts` | Core domain entity | +| `src/domain/repositories/backup-share.repository.interface.ts` | Repository interface | +| `src/application/commands/store-backup-share/` | Store use case | +| `src/application/queries/get-backup-share/` | Retrieve use case | +| `src/infrastructure/crypto/aes-encryption.service.ts` | Encryption service | +| `src/shared/guards/service-auth.guard.ts` | Authentication guard | +| `prisma/schema.prisma` | Database schema | diff --git a/backend/services/backup-service/docs/DEPLOYMENT.md b/backend/services/backup-service/docs/DEPLOYMENT.md new file mode 100644 index 00000000..bc668602 --- /dev/null +++ b/backend/services/backup-service/docs/DEPLOYMENT.md @@ -0,0 +1,696 @@ +# Backup Service Deployment Guide + +## Overview + +This guide covers deploying the backup-service to various environments. The service is designed to run in Docker containers with PostgreSQL as the database. + +**Critical Security Requirement:** The backup-service MUST be deployed on a physically separate server from identity-service to maintain MPC security. + +--- + +## Deployment Architecture + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ Production Architecture │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ Server A (Identity) Server B (Backup) │ +│ ┌─────────────────────┐ ┌─────────────────────┐ │ +│ │ identity-service │ │ backup-service │ │ +│ │ ┌───────────────┐ │ │ ┌───────────────┐ │ │ +│ │ │ PostgreSQL │ │ │ │ PostgreSQL │ │ │ +│ │ │ (identity-db) │ │ │ │ (backup-db) │ │ │ +│ │ └───────────────┘ │ │ └───────────────┘ │ │ +│ └─────────────────────┘ └─────────────────────┘ │ +│ │ ▲ │ +│ │ Internal Network │ │ +│ └───────────────────────────────┘ │ +│ (Service-to-Service JWT) │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## Docker Deployment + +### Production Dockerfile + +```dockerfile +# Dockerfile +# Multi-stage build for smaller image size + +# Stage 1: Build +FROM node:20-alpine AS builder + +WORKDIR /app + +# Copy package files +COPY package*.json ./ +COPY prisma ./prisma/ + +# Install all dependencies (including devDependencies for build) +RUN npm ci + +# Copy source code +COPY . . + +# Generate Prisma client +RUN npx prisma generate + +# Build application +RUN npm run build + +# Stage 2: Production +FROM node:20-alpine AS production + +WORKDIR /app + +# Create non-root user for security +RUN addgroup -g 1001 -S nodejs && \ + adduser -S nestjs -u 1001 + +# Copy package files +COPY package*.json ./ + +# Install production dependencies only +RUN npm ci --only=production && npm cache clean --force + +# Copy built application +COPY --from=builder /app/dist ./dist +COPY --from=builder /app/node_modules/.prisma ./node_modules/.prisma +COPY --from=builder /app/prisma ./prisma + +# Change ownership to non-root user +RUN chown -R nestjs:nodejs /app + +# Switch to non-root user +USER nestjs + +# Expose port +EXPOSE 3002 + +# Health check +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD wget --no-verbose --tries=1 --spider http://localhost:3002/health || exit 1 + +# Start application +CMD ["node", "dist/main.js"] +``` + +### Build and Push Image + +```bash +# Build image +docker build -t rwa-durian/backup-service:latest . + +# Tag for registry +docker tag rwa-durian/backup-service:latest registry.example.com/backup-service:v1.0.0 + +# Push to registry +docker push registry.example.com/backup-service:v1.0.0 +``` + +--- + +## Docker Compose Deployment + +### Production Compose File + +```yaml +# docker-compose.prod.yml +version: '3.8' + +services: + backup-service: + image: rwa-durian/backup-service:latest + container_name: backup-service + restart: unless-stopped + ports: + - "3002:3002" + environment: + - DATABASE_URL=postgresql://postgres:${DB_PASSWORD}@backup-db:5432/rwa_backup?schema=public + - APP_PORT=3002 + - APP_ENV=production + - SERVICE_JWT_SECRET=${SERVICE_JWT_SECRET} + - ALLOWED_SERVICES=${ALLOWED_SERVICES} + - BACKUP_ENCRYPTION_KEY=${BACKUP_ENCRYPTION_KEY} + - BACKUP_ENCRYPTION_KEY_ID=${BACKUP_ENCRYPTION_KEY_ID} + - MAX_RETRIEVE_PER_DAY=3 + - MAX_STORE_PER_MINUTE=10 + - AUDIT_LOG_RETENTION_DAYS=365 + depends_on: + backup-db: + condition: service_healthy + networks: + - backup-network + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" + deploy: + resources: + limits: + cpus: '1' + memory: 512M + reservations: + cpus: '0.5' + memory: 256M + + backup-db: + image: postgres:15-alpine + container_name: backup-db + restart: unless-stopped + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: ${DB_PASSWORD} + POSTGRES_DB: rwa_backup + volumes: + - backup-db-data:/var/lib/postgresql/data + - ./init-db.sql:/docker-entrypoint-initdb.d/init.sql:ro + ports: + - "5433:5432" # Different port to avoid conflicts + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres"] + interval: 10s + timeout: 5s + retries: 5 + networks: + - backup-network + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" + +volumes: + backup-db-data: + driver: local + +networks: + backup-network: + driver: bridge +``` + +### Environment File + +```bash +# .env.production +DB_PASSWORD=your-strong-database-password-here +SERVICE_JWT_SECRET=your-super-secret-service-jwt-key-min-32-chars +ALLOWED_SERVICES=identity-service,recovery-service +BACKUP_ENCRYPTION_KEY=your-256-bit-encryption-key-in-hex-64-chars +BACKUP_ENCRYPTION_KEY_ID=key-v1 +``` + +### Deploy Commands + +```bash +# Pull latest image +docker-compose -f docker-compose.prod.yml pull + +# Start services +docker-compose -f docker-compose.prod.yml up -d + +# Run database migrations +docker-compose -f docker-compose.prod.yml exec backup-service \ + npx prisma migrate deploy + +# View logs +docker-compose -f docker-compose.prod.yml logs -f backup-service + +# Stop services +docker-compose -f docker-compose.prod.yml down +``` + +--- + +## Kubernetes Deployment + +### Namespace and ConfigMap + +```yaml +# kubernetes/namespace.yaml +apiVersion: v1 +kind: Namespace +metadata: + name: rwa-backup + +--- +# kubernetes/configmap.yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: backup-service-config + namespace: rwa-backup +data: + APP_PORT: "3002" + APP_ENV: "production" + ALLOWED_SERVICES: "identity-service,recovery-service" + MAX_RETRIEVE_PER_DAY: "3" + MAX_STORE_PER_MINUTE: "10" + AUDIT_LOG_RETENTION_DAYS: "365" +``` + +### Secrets + +```yaml +# kubernetes/secrets.yaml +apiVersion: v1 +kind: Secret +metadata: + name: backup-service-secrets + namespace: rwa-backup +type: Opaque +stringData: + DATABASE_URL: "postgresql://postgres:password@backup-db:5432/rwa_backup?schema=public" + SERVICE_JWT_SECRET: "your-super-secret-service-jwt-key-min-32-chars" + BACKUP_ENCRYPTION_KEY: "your-256-bit-encryption-key-in-hex-64-chars" + BACKUP_ENCRYPTION_KEY_ID: "key-v1" +``` + +### Deployment + +```yaml +# kubernetes/deployment.yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: backup-service + namespace: rwa-backup +spec: + replicas: 2 + selector: + matchLabels: + app: backup-service + template: + metadata: + labels: + app: backup-service + spec: + securityContext: + runAsNonRoot: true + runAsUser: 1001 + containers: + - name: backup-service + image: registry.example.com/backup-service:v1.0.0 + ports: + - containerPort: 3002 + envFrom: + - configMapRef: + name: backup-service-config + - secretRef: + name: backup-service-secrets + resources: + requests: + memory: "256Mi" + cpu: "250m" + limits: + memory: "512Mi" + cpu: "500m" + livenessProbe: + httpGet: + path: /health/live + port: 3002 + initialDelaySeconds: 15 + periodSeconds: 20 + readinessProbe: + httpGet: + path: /health/ready + port: 3002 + initialDelaySeconds: 5 + periodSeconds: 10 +``` + +### Service + +```yaml +# kubernetes/service.yaml +apiVersion: v1 +kind: Service +metadata: + name: backup-service + namespace: rwa-backup +spec: + selector: + app: backup-service + ports: + - protocol: TCP + port: 3002 + targetPort: 3002 + type: ClusterIP +``` + +### PostgreSQL StatefulSet + +```yaml +# kubernetes/postgres.yaml +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: backup-db + namespace: rwa-backup +spec: + serviceName: backup-db + replicas: 1 + selector: + matchLabels: + app: backup-db + template: + metadata: + labels: + app: backup-db + spec: + containers: + - name: postgres + image: postgres:15-alpine + ports: + - containerPort: 5432 + env: + - name: POSTGRES_DB + value: rwa_backup + - name: POSTGRES_USER + value: postgres + - name: POSTGRES_PASSWORD + valueFrom: + secretKeyRef: + name: backup-db-secrets + key: password + volumeMounts: + - name: postgres-data + mountPath: /var/lib/postgresql/data + resources: + requests: + memory: "256Mi" + cpu: "250m" + limits: + memory: "512Mi" + cpu: "500m" + volumeClaimTemplates: + - metadata: + name: postgres-data + spec: + accessModes: ["ReadWriteOnce"] + resources: + requests: + storage: 10Gi +``` + +### Deploy to Kubernetes + +```bash +# Apply all manifests +kubectl apply -f kubernetes/ + +# Check deployment status +kubectl -n rwa-backup get pods + +# View logs +kubectl -n rwa-backup logs -f deployment/backup-service + +# Run migrations +kubectl -n rwa-backup exec -it deployment/backup-service -- \ + npx prisma migrate deploy +``` + +--- + +## Database Management + +### Initial Setup + +```bash +# Run migrations on first deployment +npx prisma migrate deploy + +# Or push schema directly (development only) +npx prisma db push +``` + +### Backup and Restore + +```bash +# Backup database +docker-compose exec backup-db pg_dump -U postgres rwa_backup > backup.sql + +# Restore database +docker-compose exec -T backup-db psql -U postgres rwa_backup < backup.sql +``` + +### Migration in Production + +```bash +# Generate migration (development) +npx prisma migrate dev --name add_new_field + +# Apply migration (production) +npx prisma migrate deploy +``` + +--- + +## Environment Variables Reference + +### Required Variables + +| Variable | Description | Example | +|----------|-------------|---------| +| `DATABASE_URL` | PostgreSQL connection string | `postgresql://user:pass@host:5432/db` | +| `SERVICE_JWT_SECRET` | JWT secret for service auth (min 32 chars) | Random 64+ char string | +| `ALLOWED_SERVICES` | Comma-separated allowed services | `identity-service,recovery-service` | +| `BACKUP_ENCRYPTION_KEY` | 256-bit key in hex (64 chars) | 64 hex characters | +| `BACKUP_ENCRYPTION_KEY_ID` | Key identifier | `key-v1` | + +### Optional Variables + +| Variable | Default | Description | +|----------|---------|-------------| +| `APP_PORT` | `3002` | Server port | +| `APP_ENV` | `development` | Environment (development/production) | +| `MAX_RETRIEVE_PER_DAY` | `3` | Max retrieves per user per day | +| `MAX_STORE_PER_MINUTE` | `10` | Max stores per minute | +| `AUDIT_LOG_RETENTION_DAYS` | `365` | Audit log retention period | + +--- + +## Security Considerations + +### Network Security + +1. **Isolate backup-service network** + - Use private subnets + - Restrict access to identity-service only + - Use VPN or VPC peering for cross-server communication + +2. **Firewall rules** + ```bash + # Allow only identity-service IP + iptables -A INPUT -p tcp --dport 3002 -s identity-service-ip -j ACCEPT + iptables -A INPUT -p tcp --dport 3002 -j DROP + ``` + +3. **TLS/SSL** + - Use reverse proxy (nginx/traefik) for TLS termination + - Enable mutual TLS for service-to-service communication + +### Secret Management + +1. **Use secret management services** + - AWS Secrets Manager + - HashiCorp Vault + - Kubernetes Secrets with encryption at rest + +2. **Rotate secrets regularly** + - Rotate encryption keys annually + - Rotate JWT secrets quarterly + - Use key versioning for encryption keys + +### Database Security + +1. **Use strong passwords** +2. **Enable SSL for database connections** +3. **Regular backups with encryption** +4. **Limit database user permissions** + +--- + +## Monitoring and Logging + +### Health Endpoints + +| Endpoint | Purpose | +|----------|---------| +| `GET /health` | Basic health check | +| `GET /health/ready` | Readiness probe (includes DB check) | +| `GET /health/live` | Liveness probe | + +### Prometheus Metrics (Optional) + +```yaml +# Add to deployment +- name: PROMETHEUS_ENABLED + value: "true" +- name: PROMETHEUS_PORT + value: "9102" +``` + +### Log Aggregation + +Configure log driver for centralized logging: + +```yaml +logging: + driver: "fluentd" + options: + fluentd-address: "localhost:24224" + tag: "backup-service" +``` + +--- + +## Troubleshooting + +### Common Issues + +#### Service won't start + +```bash +# Check logs +docker-compose logs backup-service + +# Common causes: +# 1. Database not ready +# 2. Missing environment variables +# 3. Invalid encryption key format +``` + +#### Database connection failed + +```bash +# Check database is running +docker-compose ps backup-db + +# Check database logs +docker-compose logs backup-db + +# Test connection +docker-compose exec backup-service \ + npx prisma db pull +``` + +#### Authentication errors + +```bash +# Verify JWT secret matches between services +# Check ALLOWED_SERVICES includes calling service +# Verify token format and expiration +``` + +### Recovery Procedures + +#### Database Recovery + +```bash +# Stop service +docker-compose stop backup-service + +# Restore from backup +docker-compose exec -T backup-db psql -U postgres rwa_backup < backup.sql + +# Run migrations +docker-compose exec backup-service npx prisma migrate deploy + +# Start service +docker-compose start backup-service +``` + +#### Key Rotation + +1. Add new key to encryption service +2. Re-encrypt existing data with new key +3. Update `BACKUP_ENCRYPTION_KEY_ID` +4. Remove old key after transition period + +--- + +## Scaling + +### Horizontal Scaling + +The service is stateless and can be horizontally scaled: + +```yaml +# Docker Compose scale +docker-compose up -d --scale backup-service=3 + +# Kubernetes replicas +kubectl -n rwa-backup scale deployment/backup-service --replicas=3 +``` + +### Load Balancing + +Use a load balancer in front of multiple instances: + +```nginx +# nginx.conf +upstream backup_service { + least_conn; + server backup-service-1:3002; + server backup-service-2:3002; + server backup-service-3:3002; +} + +server { + listen 443 ssl; + server_name backup-api.example.com; + + location / { + proxy_pass http://backup_service; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + } +} +``` + +### Database Scaling + +For high availability: + +1. Use managed PostgreSQL (AWS RDS, GCP Cloud SQL) +2. Configure read replicas for read scaling +3. Use connection pooling (PgBouncer) + +--- + +## Maintenance + +### Regular Tasks + +| Task | Frequency | Command | +|------|-----------|---------| +| Database backup | Daily | `pg_dump rwa_backup > backup.sql` | +| Log rotation | Weekly | Automatic with log driver config | +| Security updates | Monthly | Rebuild and redeploy image | +| Audit log cleanup | Monthly | `DELETE FROM share_access_logs WHERE created_at < NOW() - INTERVAL '365 days'` | + +### Update Procedure + +```bash +# 1. Build new image +docker build -t rwa-durian/backup-service:v1.1.0 . + +# 2. Push to registry +docker push registry.example.com/backup-service:v1.1.0 + +# 3. Update deployment +docker-compose pull +docker-compose up -d + +# 4. Run migrations if needed +docker-compose exec backup-service npx prisma migrate deploy + +# 5. Verify health +curl http://localhost:3002/health +``` diff --git a/backend/services/backup-service/docs/DEVELOPMENT.md b/backend/services/backup-service/docs/DEVELOPMENT.md new file mode 100644 index 00000000..69e96880 --- /dev/null +++ b/backend/services/backup-service/docs/DEVELOPMENT.md @@ -0,0 +1,513 @@ +# Backup Service Development Guide + +## Prerequisites + +- **Node.js:** v20.x or higher +- **npm:** v10.x or higher +- **Docker:** For running PostgreSQL locally +- **WSL2:** (Windows users) For running Docker and tests + +--- + +## Quick Start + +### 1. Clone and Install + +```bash +# Navigate to service directory +cd backend/services/backup-service + +# Install dependencies +npm install +``` + +### 2. Environment Setup + +Copy the example environment file: + +```bash +cp .env.example .env +``` + +Configure the following environment variables: + +```bash +# Database Configuration +DATABASE_URL="postgresql://postgres:password@localhost:5433/rwa_backup?schema=public" + +# Server Configuration +APP_PORT=3002 +APP_ENV="development" + +# Service-to-Service Authentication +SERVICE_JWT_SECRET="your-super-secret-service-jwt-key-min-32-chars" +ALLOWED_SERVICES="identity-service,recovery-service" + +# Encryption +BACKUP_ENCRYPTION_KEY="your-256-bit-encryption-key-in-hex-64-chars" +BACKUP_ENCRYPTION_KEY_ID="key-v1" + +# Rate Limiting +MAX_RETRIEVE_PER_DAY=3 +MAX_STORE_PER_MINUTE=10 + +# Audit +AUDIT_LOG_RETENTION_DAYS=365 +``` + +### 3. Start Database + +```bash +# Start PostgreSQL container +docker-compose up -d + +# Verify database is running +docker-compose ps +``` + +### 4. Setup Database Schema + +```bash +# Generate Prisma client +npm run prisma:generate + +# Run migrations +npm run prisma:migrate +``` + +### 5. Start Development Server + +```bash +# Start with hot-reload +npm run start:dev + +# Or start in debug mode +npm run start:debug +``` + +The service will be available at `http://localhost:3002`. + +--- + +## Project Scripts + +### Development + +| Script | Description | +|--------|-------------| +| `npm run start` | Start the service | +| `npm run start:dev` | Start with hot-reload | +| `npm run start:debug` | Start with debugger | +| `npm run start:prod` | Start production build | +| `npm run build` | Build for production | + +### Database + +| Script | Description | +|--------|-------------| +| `npm run prisma:generate` | Generate Prisma client | +| `npm run prisma:migrate` | Run development migrations | +| `npm run prisma:migrate:prod` | Run production migrations | +| `npm run prisma:studio` | Open Prisma Studio GUI | + +### Testing + +| Script | Description | +|--------|-------------| +| `npm run test` | Run all tests | +| `npm run test:unit` | Run unit tests only | +| `npm run test:e2e:mock` | Run E2E tests with mocks | +| `npm run test:e2e:db` | Run E2E tests with real DB | +| `npm run test:cov` | Generate coverage report | +| `npm run test:watch` | Run tests in watch mode | + +### Docker + +| Script | Description | +|--------|-------------| +| `npm run docker:build` | Build Docker image | +| `npm run docker:up` | Start Docker compose stack | +| `npm run docker:down` | Stop Docker compose stack | +| `npm run db:test:up` | Start test database | +| `npm run db:test:down` | Stop test database | + +--- + +## Code Structure + +### Adding a New Feature + +Follow the DDD + Hexagonal architecture pattern: + +#### 1. Domain Layer (Core Business Logic) + +Create entities and value objects first: + +```typescript +// src/domain/entities/new-entity.entity.ts +export class NewEntity { + // Properties + private id: bigint; + private data: string; + + // Factory method + static create(params: CreateParams): NewEntity { + // Validation and creation logic + return new NewEntity(params); + } + + // Domain methods + performAction(): void { + // Business logic + } +} +``` + +#### 2. Repository Interface + +Define the data access contract: + +```typescript +// src/domain/repositories/new-entity.repository.interface.ts +export interface NewEntityRepository { + save(entity: NewEntity): Promise; + findById(id: bigint): Promise; + // ... other methods +} +``` + +#### 3. Application Layer (Use Cases) + +Create commands/queries and handlers: + +```typescript +// src/application/commands/create-new-entity/create-new-entity.command.ts +export class CreateNewEntityCommand { + constructor( + public readonly data: string, + // ... other fields + ) {} +} + +// src/application/commands/create-new-entity/create-new-entity.handler.ts +@Injectable() +export class CreateNewEntityHandler { + constructor( + @Inject('NewEntityRepository') + private readonly repository: NewEntityRepository, + ) {} + + async execute(command: CreateNewEntityCommand): Promise { + const entity = NewEntity.create({ data: command.data }); + await this.repository.save(entity); + return { id: entity.id.toString() }; + } +} +``` + +#### 4. Infrastructure Layer + +Implement the repository: + +```typescript +// src/infrastructure/persistence/repositories/new-entity.repository.impl.ts +@Injectable() +export class NewEntityRepositoryImpl implements NewEntityRepository { + constructor(private readonly prisma: PrismaService) {} + + async save(entity: NewEntity): Promise { + const data = await this.prisma.newEntity.create({ + data: this.toDatabase(entity), + }); + return this.toDomain(data); + } + + // Mapping methods + private toDatabase(entity: NewEntity) { /* ... */ } + private toDomain(data: PrismaModel) { /* ... */ } +} +``` + +#### 5. API Layer + +Create controller and DTOs: + +```typescript +// src/api/dto/request/create-new-entity.dto.ts +export class CreateNewEntityDto { + @IsNotEmpty() + @IsString() + data: string; +} + +// src/api/controllers/new-entity.controller.ts +@Controller('new-entity') +@UseGuards(ServiceAuthGuard) +export class NewEntityController { + constructor( + private readonly service: NewEntityApplicationService, + ) {} + + @Post() + async create(@Body() dto: CreateNewEntityDto) { + return this.service.create(dto); + } +} +``` + +--- + +## Environment Variables + +### Required Variables + +| Variable | Description | Example | +|----------|-------------|---------| +| `DATABASE_URL` | PostgreSQL connection string | `postgresql://...` | +| `SERVICE_JWT_SECRET` | JWT secret (min 32 chars) | `your-secret-key...` | +| `ALLOWED_SERVICES` | Comma-separated service list | `identity-service,recovery-service` | +| `BACKUP_ENCRYPTION_KEY` | 256-bit key in hex (64 chars) | `0123456789abcdef...` | +| `BACKUP_ENCRYPTION_KEY_ID` | Key identifier for rotation | `key-v1` | + +### Optional Variables + +| Variable | Default | Description | +|----------|---------|-------------| +| `APP_PORT` | `3002` | Server port | +| `APP_ENV` | `development` | Environment mode | +| `MAX_RETRIEVE_PER_DAY` | `3` | Rate limit for retrieves | +| `MAX_STORE_PER_MINUTE` | `10` | Rate limit for stores | +| `AUDIT_LOG_RETENTION_DAYS` | `365` | Audit log retention | + +--- + +## Database Management + +### Prisma CLI Commands + +```bash +# Generate Prisma client after schema changes +npx prisma generate + +# Create a new migration +npx prisma migrate dev --name migration_name + +# Apply migrations in production +npx prisma migrate deploy + +# Reset database (development only) +npx prisma migrate reset + +# Open Prisma Studio +npx prisma studio + +# Push schema without migrations (development) +npx prisma db push +``` + +### Schema Changes Workflow + +1. Modify `prisma/schema.prisma` +2. Create migration: `npx prisma migrate dev --name descriptive_name` +3. Test locally +4. Commit migration files +5. Apply in staging/production: `npx prisma migrate deploy` + +--- + +## Debugging + +### VSCode Launch Configuration + +Add to `.vscode/launch.json`: + +```json +{ + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "launch", + "name": "Debug Nest.js", + "runtimeArgs": [ + "-r", + "ts-node/register", + "-r", + "tsconfig-paths/register" + ], + "args": ["${workspaceFolder}/src/main.ts"], + "envFile": "${workspaceFolder}/.env" + }, + { + "type": "node", + "request": "launch", + "name": "Debug Jest Tests", + "program": "${workspaceFolder}/node_modules/.bin/jest", + "args": ["--runInBand", "--config", "jest.config.js"], + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen" + } + ] +} +``` + +### Debug Logging + +Enable verbose logging: + +```typescript +// In main.ts or app.module.ts +app.useLogger(['log', 'error', 'warn', 'debug', 'verbose']); +``` + +--- + +## Code Style + +### TypeScript Configuration + +The project uses strict TypeScript settings: + +```json +{ + "compilerOptions": { + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "noUnusedLocals": true, + "noUnusedParameters": true + } +} +``` + +### Naming Conventions + +| Type | Convention | Example | +|------|------------|---------| +| Files | kebab-case | `backup-share.entity.ts` | +| Classes | PascalCase | `BackupShareEntity` | +| Interfaces | PascalCase with I prefix | `IBackupShareRepository` | +| Functions | camelCase | `storeBackupShare()` | +| Constants | UPPER_SNAKE_CASE | `MAX_RETRY_COUNT` | +| Environment | UPPER_SNAKE_CASE | `DATABASE_URL` | + +### File Organization + +``` +feature/ +├── feature.command.ts # Command object +├── feature.handler.ts # Command handler +├── feature.spec.ts # Unit tests +└── index.ts # Barrel export +``` + +--- + +## Common Development Tasks + +### Adding a New Endpoint + +1. Create DTO in `src/api/dto/request/` +2. Add validation decorators +3. Create handler in `src/application/commands/` or `queries/` +4. Add method to controller +5. Write unit tests +6. Write E2E tests + +### Adding a New Environment Variable + +1. Add to `.env.example` with description +2. Add to `src/config/index.ts` +3. Update this documentation +4. Update deployment configs + +### Updating Database Schema + +1. Modify `prisma/schema.prisma` +2. Run `npx prisma migrate dev --name description` +3. Update domain entities if needed +4. Update repository mappings +5. Write migration tests + +--- + +## Troubleshooting + +### Common Issues + +#### Prisma Client Not Generated + +```bash +Error: @prisma/client did not initialize yet +``` + +**Solution:** +```bash +npm run prisma:generate +``` + +#### Database Connection Failed + +```bash +Error: Can't reach database server +``` + +**Solution:** +1. Check if Docker is running: `docker ps` +2. Check DATABASE_URL in `.env` +3. Restart database: `docker-compose restart backup-db` + +#### Port Already in Use + +```bash +Error: listen EADDRINUSE: address already in use :::3002 +``` + +**Solution:** +```bash +# Find and kill the process +netstat -ano | findstr :3002 +taskkill /PID /F +``` + +#### TypeScript Compilation Errors + +```bash +# Clear build cache +rm -rf dist +npm run build +``` + +### Getting Help + +- Check existing issues on GitHub +- Review NestJS documentation +- Review Prisma documentation +- Ask in team Slack channel + +--- + +## Best Practices + +### Security + +1. Never commit secrets to git +2. Use environment variables for all configs +3. Validate all inputs with class-validator +4. Sanitize logs (no sensitive data) +5. Use parameterized queries (Prisma handles this) + +### Performance + +1. Use database indexes for frequently queried fields +2. Implement pagination for list endpoints +3. Use connection pooling (Prisma default) +4. Cache frequently accessed data if needed + +### Code Quality + +1. Write unit tests for all handlers +2. Write E2E tests for all endpoints +3. Use TypeScript strict mode +4. Follow DDD principles +5. Keep controllers thin, logic in handlers diff --git a/backend/services/backup-service/docs/README.md b/backend/services/backup-service/docs/README.md new file mode 100644 index 00000000..250d38be --- /dev/null +++ b/backend/services/backup-service/docs/README.md @@ -0,0 +1,77 @@ +# Backup Service Documentation + +Welcome to the backup-service documentation. This service is responsible for securely storing MPC backup shares (Party 2/3) for the RWA Durian platform. + +## Documentation Index + +| Document | Description | +|----------|-------------| +| [ARCHITECTURE.md](./ARCHITECTURE.md) | DDD + Hexagonal architecture, design patterns, directory structure, domain layer details | +| [API.md](./API.md) | API endpoints reference, authentication, request/response formats, SDK examples | +| [DEVELOPMENT.md](./DEVELOPMENT.md) | Development setup, environment configuration, adding features, debugging | +| [TESTING.md](./TESTING.md) | Unit tests, E2E tests, test utilities, running tests, writing good tests | +| [DEPLOYMENT.md](./DEPLOYMENT.md) | Docker, Kubernetes deployment, environment variables, security, monitoring | + +## Quick Links + +### Getting Started + +1. [Development Setup](./DEVELOPMENT.md#quick-start) +2. [Environment Variables](./DEVELOPMENT.md#environment-variables) +3. [Running Tests](./TESTING.md#running-tests) + +### API Reference + +1. [Store Backup Share](./API.md#1-store-backup-share) +2. [Retrieve Backup Share](./API.md#2-retrieve-backup-share) +3. [Revoke Backup Share](./API.md#3-revoke-backup-share) +4. [Health Endpoints](./API.md#4-health-check) + +### Architecture + +1. [Hexagonal Architecture](./ARCHITECTURE.md#ddd--hexagonal-architecture) +2. [Domain Layer](./ARCHITECTURE.md#domain-layer-details) +3. [Database Schema](./ARCHITECTURE.md#database-schema) +4. [Key Decisions](./ARCHITECTURE.md#key-architectural-decisions) + +### Deployment + +1. [Docker Deployment](./DEPLOYMENT.md#docker-deployment) +2. [Kubernetes Deployment](./DEPLOYMENT.md#kubernetes-deployment) +3. [Security Considerations](./DEPLOYMENT.md#security-considerations) + +## Service Overview + +**Purpose:** Securely store and manage MPC backup shares (Party 2) for account recovery + +**Key Features:** +- Double encryption (AES-256-GCM) +- Service-to-service JWT authentication +- Rate limiting (3 retrieves per user per day) +- Comprehensive audit logging +- Physical server isolation from identity-service + +**Technology Stack:** +- NestJS 11.x (TypeScript) +- Prisma 7.x ORM +- PostgreSQL 15 +- Docker / Kubernetes + +## Test Summary + +| Category | Tests | +|----------|-------| +| Unit Tests | 37 | +| Mock E2E Tests | 21 | +| Real DB E2E Tests | 20 | +| **Total** | **78** | + +## Critical Security Note + +The backup-service MUST be deployed on a **physically separate server** from identity-service. This is mandatory for maintaining MPC security: + +- Party 0 (Server Share): identity-service (Server A) +- Party 1 (Client Share): User device +- Party 2 (Backup Share): backup-service (Server B) + +If only one server is compromised, attackers can only obtain 1 of 3 shares, making key reconstruction impossible (2-of-3 threshold). diff --git a/backend/services/backup-service/docs/TESTING.md b/backend/services/backup-service/docs/TESTING.md new file mode 100644 index 00000000..02fd48eb --- /dev/null +++ b/backend/services/backup-service/docs/TESTING.md @@ -0,0 +1,809 @@ +# Backup Service Testing Guide + +## Overview + +The backup-service implements a comprehensive testing strategy with three levels: + +1. **Unit Tests** - Test individual components in isolation +2. **Integration Tests** - Test component interactions with real database +3. **E2E Tests** - Test complete API workflows + +--- + +## Test Structure + +``` +test/ +├── unit/ # Unit tests (37 tests) +│ ├── api/ +│ │ ├── backup-share.controller.spec.ts +│ │ └── health.controller.spec.ts +│ ├── application/ +│ │ ├── store-backup-share.handler.spec.ts +│ │ ├── get-backup-share.handler.spec.ts +│ │ └── revoke-share.handler.spec.ts +│ ├── domain/ +│ │ ├── backup-share.entity.spec.ts +│ │ └── value-objects.spec.ts +│ ├── infrastructure/ +│ │ └── aes-encryption.service.spec.ts +│ └── shared/ +│ ├── audit-log.interceptor.spec.ts +│ ├── global-exception.filter.spec.ts +│ └── service-auth.guard.spec.ts +│ +├── integration/ # Integration tests +│ ├── backup-share-repository.integration.spec.ts +│ └── audit-log-repository.integration.spec.ts +│ +├── e2e/ # E2E tests (20 tests) +│ ├── backup-share.e2e-spec.ts # Real database +│ └── backup-share-mock.e2e-spec.ts # Mocked services +│ +├── setup/ # Test infrastructure +│ ├── global-setup.ts +│ ├── global-teardown.ts +│ ├── jest-e2e-setup.ts +│ ├── jest-mock-setup.ts +│ └── test-database.helper.ts +│ +└── utils/ # Test utilities + ├── mock-prisma.service.ts + └── test-utils.ts +``` + +--- + +## Running Tests + +### Quick Commands + +```bash +# Run all unit tests +npm run test:unit + +# Run E2E tests with mocked services (fast) +npm run test:e2e:mock + +# Run E2E tests with real database +npm run test:e2e:db + +# Run all tests (unit + mock E2E) +npm run test:all + +# Generate coverage report +npm run test:cov + +# Watch mode for development +npm run test:watch +``` + +### Test Configurations + +| Config File | Purpose | Command | +|------------|---------|---------| +| `jest.config.js` | Unit tests | `npm run test:unit` | +| `test/jest-e2e-mock.json` | E2E with mocks | `npm run test:e2e:mock` | +| `test/jest-e2e-db.json` | E2E with real DB | `npm run test:e2e:db` | + +--- + +## Unit Testing + +### Philosophy + +- Test each component in isolation +- Mock all dependencies +- Focus on business logic and edge cases +- Fast execution (< 5 seconds total) + +### Example: Testing a Handler + +```typescript +// test/unit/application/store-backup-share.handler.spec.ts +import { Test, TestingModule } from '@nestjs/testing'; +import { StoreBackupShareHandler } from '../../../src/application/commands/store-backup-share/store-backup-share.handler'; +import { StoreBackupShareCommand } from '../../../src/application/commands/store-backup-share/store-backup-share.command'; +import { BackupShareRepository } from '../../../src/domain/repositories/backup-share.repository.interface'; +import { AesEncryptionService } from '../../../src/infrastructure/crypto/aes-encryption.service'; +import { AuditLogRepository } from '../../../src/infrastructure/persistence/repositories/audit-log.repository'; + +describe('StoreBackupShareHandler', () => { + let handler: StoreBackupShareHandler; + let mockRepository: jest.Mocked; + let mockEncryptionService: jest.Mocked; + let mockAuditLogRepository: jest.Mocked; + + beforeEach(async () => { + mockRepository = { + save: jest.fn(), + findByUserId: jest.fn(), + findByPublicKey: jest.fn(), + // ... other methods + }; + + mockEncryptionService = { + encrypt: jest.fn().mockResolvedValue({ + encrypted: 'encrypted-data', + keyId: 'key-v1', + }), + }; + + mockAuditLogRepository = { + log: jest.fn().mockResolvedValue(undefined), + }; + + const module: TestingModule = await Test.createTestingModule({ + providers: [ + StoreBackupShareHandler, + { provide: 'BackupShareRepository', useValue: mockRepository }, + { provide: AesEncryptionService, useValue: mockEncryptionService }, + { provide: AuditLogRepository, useValue: mockAuditLogRepository }, + ], + }).compile(); + + handler = module.get(StoreBackupShareHandler); + }); + + describe('execute', () => { + it('should store backup share successfully', async () => { + // Arrange + const command = new StoreBackupShareCommand( + '12345', + 1001, + '02' + 'a'.repeat(64), + 'encrypted-share-data', + 'identity-service', + '127.0.0.1' + ); + + mockRepository.findByUserId.mockResolvedValue(null); + mockRepository.findByPublicKey.mockResolvedValue(null); + mockRepository.save.mockResolvedValue({ + shareId: BigInt(1), + // ... other fields + }); + + // Act + const result = await handler.execute(command); + + // Assert + expect(result.shareId).toBe('1'); + expect(mockRepository.save).toHaveBeenCalled(); + expect(mockEncryptionService.encrypt).toHaveBeenCalledWith('encrypted-share-data'); + expect(mockAuditLogRepository.log).toHaveBeenCalled(); + }); + + it('should throw error if share already exists for user', async () => { + // Arrange + const command = new StoreBackupShareCommand(/*...*/); + mockRepository.findByUserId.mockResolvedValue({ /* existing share */ }); + + // Act & Assert + await expect(handler.execute(command)).rejects.toThrow('SHARE_ALREADY_EXISTS'); + }); + }); +}); +``` + +### Example: Testing a Controller + +```typescript +// test/unit/api/backup-share.controller.spec.ts +describe('BackupShareController', () => { + let controller: BackupShareController; + let mockService: jest.Mocked; + + beforeEach(async () => { + mockService = { + storeBackupShare: jest.fn(), + getBackupShare: jest.fn(), + revokeShare: jest.fn(), + }; + + const module: TestingModule = await Test.createTestingModule({ + controllers: [BackupShareController], + providers: [ + { provide: BackupShareApplicationService, useValue: mockService }, + ], + }).compile(); + + controller = module.get(BackupShareController); + }); + + describe('storeShare', () => { + it('should return success response', async () => { + // Arrange + const dto: StoreShareDto = { + userId: '12345', + accountSequence: 1001, + publicKey: '02' + 'a'.repeat(64), + encryptedShareData: 'test-data', + }; + + const mockRequest = { + sourceService: 'identity-service', + sourceIp: '127.0.0.1', + }; + + mockService.storeBackupShare.mockResolvedValue({ shareId: '1' }); + + // Act + const result = await controller.storeShare(dto, mockRequest); + + // Assert + expect(result.success).toBe(true); + expect(result.shareId).toBe('1'); + }); + }); +}); +``` + +### Example: Testing Domain Entity + +```typescript +// test/unit/domain/backup-share.entity.spec.ts +describe('BackupShare Entity', () => { + describe('create', () => { + it('should create a valid backup share', () => { + const share = BackupShare.create({ + userId: BigInt(12345), + accountSequence: BigInt(1001), + publicKey: '02' + 'a'.repeat(64), + encryptedShareData: 'encrypted-data', + encryptionKeyId: 'key-v1', + }); + + expect(share.userId).toBe(BigInt(12345)); + expect(share.status).toBe(BackupShareStatus.ACTIVE); + expect(share.partyIndex).toBe(2); + expect(share.accessCount).toBe(0); + }); + + it('should throw error for invalid public key length', () => { + expect(() => BackupShare.create({ + // ... with short publicKey + publicKey: 'short', + })).toThrow(); + }); + }); + + describe('revoke', () => { + it('should mark share as revoked', () => { + const share = BackupShare.create({/*...*/}); + + share.revoke('ROTATION'); + + expect(share.status).toBe(BackupShareStatus.REVOKED); + expect(share.revokedAt).toBeDefined(); + }); + }); + + describe('recordAccess', () => { + it('should increment access count', () => { + const share = BackupShare.create({/*...*/}); + + share.recordAccess(); + share.recordAccess(); + + expect(share.accessCount).toBe(2); + expect(share.lastAccessedAt).toBeDefined(); + }); + }); +}); +``` + +--- + +## E2E Testing + +### Mock E2E Tests + +Fast tests using mocked Prisma service: + +```typescript +// test/e2e/backup-share-mock.e2e-spec.ts +describe('BackupShare E2E (Mocked)', () => { + let app: INestApplication; + let mockPrismaService: MockPrismaService; + + beforeAll(async () => { + const moduleFixture = await Test.createTestingModule({ + imports: [AppModule], + }) + .overrideProvider(PrismaService) + .useClass(MockPrismaService) + .compile(); + + app = moduleFixture.createNestApplication(); + app.useGlobalPipes(new ValidationPipe({ whitelist: true })); + await app.init(); + + mockPrismaService = app.get(MockPrismaService); + }); + + it('should store backup share successfully', async () => { + mockPrismaService.backupShare.findUnique.mockResolvedValue(null); + mockPrismaService.backupShare.create.mockResolvedValue({ + shareId: BigInt(1), + // ... mock data + }); + + const response = await request(app.getHttpServer()) + .post('/backup-share/store') + .set('X-Service-Token', generateServiceToken('identity-service')) + .send({ + userId: '12345', + accountSequence: 1001, + publicKey: '02' + 'a'.repeat(64), + encryptedShareData: 'test-data', + }) + .expect(201); + + expect(response.body.success).toBe(true); + }); +}); +``` + +### Real Database E2E Tests + +Tests with actual PostgreSQL database: + +```typescript +// test/e2e/backup-share.e2e-spec.ts +describe('BackupShare E2E (Real Database)', () => { + let app: INestApplication; + let prisma: PrismaService; + let serviceToken: string; + + beforeAll(async () => { + process.env.SERVICE_JWT_SECRET = TEST_SERVICE_JWT_SECRET; + + const moduleFixture = await Test.createTestingModule({ + imports: [AppModule], + }).compile(); + + app = moduleFixture.createNestApplication(); + app.useGlobalPipes(new ValidationPipe({ + whitelist: true, + forbidNonWhitelisted: true, + transform: true, + })); + await app.init(); + + prisma = app.get(PrismaService); + serviceToken = generateServiceToken('identity-service'); + }); + + beforeEach(async () => { + // Clean database before each test + await prisma.shareAccessLog.deleteMany(); + await prisma.backupShare.deleteMany(); + }); + + afterAll(async () => { + await app?.close(); + }); + + describe('Complete Workflow', () => { + it('should complete full lifecycle: store -> retrieve -> revoke', async () => { + const publicKey = generatePublicKey('x'); + const payload = createStoreSharePayload({ + userId: '50001', + accountSequence: 50001, + publicKey, + }); + + // Step 1: Store + const storeResponse = await request(app.getHttpServer()) + .post('/backup-share/store') + .set('X-Service-Token', serviceToken) + .send(payload) + .expect(201); + + expect(storeResponse.body.success).toBe(true); + expect(storeResponse.body.shareId).toBeDefined(); + + // Step 2: Retrieve + const retrieveResponse = await request(app.getHttpServer()) + .post('/backup-share/retrieve') + .set('X-Service-Token', serviceToken) + .send(createRetrieveSharePayload({ userId: '50001', publicKey })) + .expect(200); + + expect(retrieveResponse.body.success).toBe(true); + expect(retrieveResponse.body.partyIndex).toBe(2); + + // Step 3: Revoke + const revokeResponse = await request(app.getHttpServer()) + .post('/backup-share/revoke') + .set('X-Service-Token', serviceToken) + .send(createRevokeSharePayload({ userId: '50001', publicKey, reason: 'ROTATION' })) + .expect(200); + + expect(revokeResponse.body.success).toBe(true); + + // Step 4: Verify cannot retrieve after revoke + await request(app.getHttpServer()) + .post('/backup-share/retrieve') + .set('X-Service-Token', serviceToken) + .send(createRetrieveSharePayload({ userId: '50001', publicKey })) + .expect(400); + }); + }); +}); +``` + +--- + +## Test Utilities + +### test-utils.ts + +```typescript +// test/utils/test-utils.ts +import jwt from 'jsonwebtoken'; + +export const TEST_SERVICE_JWT_SECRET = 'test-super-secret-service-jwt-key-for-e2e-testing'; +export const TEST_ENCRYPTION_KEY = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef'; +export const TEST_ENCRYPTION_KEY_ID = 'test-key-v1'; + +// Generate service JWT token +export function generateServiceToken( + service: string, + secret: string = TEST_SERVICE_JWT_SECRET, + expiresIn: string = '1h' +): string { + return jwt.sign({ service }, secret, { expiresIn }); +} + +// Generate expired token for testing +export function generateExpiredServiceToken( + service: string, + secret: string = TEST_SERVICE_JWT_SECRET +): string { + return jwt.sign({ service }, secret, { expiresIn: '-1h' }); +} + +// Generate valid public key (66 chars for compressed) +export function generatePublicKey(prefix: string = 'a'): string { + return '02' + prefix.repeat(64); +} + +// Create store share payload with defaults +export function createStoreSharePayload(overrides: Partial = {}): StoreShareDto { + return { + userId: '12345', + accountSequence: 1001, + publicKey: generatePublicKey('a'), + encryptedShareData: 'test-encrypted-share-data', + ...overrides, + }; +} + +// Create retrieve share payload with defaults +export function createRetrieveSharePayload(overrides: Partial = {}): RetrieveShareDto { + return { + userId: '12345', + publicKey: generatePublicKey('a'), + recoveryToken: 'valid-recovery-token', + ...overrides, + }; +} + +// Create revoke share payload with defaults +export function createRevokeSharePayload(overrides: Partial = {}): RevokeShareDto { + return { + userId: '12345', + publicKey: generatePublicKey('a'), + reason: 'ROTATION', + ...overrides, + }; +} +``` + +### Mock Prisma Service + +```typescript +// test/utils/mock-prisma.service.ts +export class MockPrismaService { + backupShare = { + create: jest.fn(), + findUnique: jest.fn(), + findFirst: jest.fn(), + update: jest.fn(), + delete: jest.fn(), + deleteMany: jest.fn(), + }; + + shareAccessLog = { + create: jest.fn(), + findMany: jest.fn(), + count: jest.fn(), + deleteMany: jest.fn(), + }; + + $connect = jest.fn(); + $disconnect = jest.fn(); +} +``` + +--- + +## Running E2E Tests with Real Database + +### Using WSL (Windows) + +```bash +# 1. Start test database in WSL +wsl docker run -d \ + --name backup-service-test-db \ + -e POSTGRES_USER=postgres \ + -e POSTGRES_PASSWORD=testpassword \ + -e POSTGRES_DB=rwa_backup_test \ + -p 5434:5432 \ + postgres:15-alpine + +# 2. Wait for database to be ready +wsl docker logs -f backup-service-test-db + +# 3. Run tests (from Windows) +npm run test:e2e:db + +# 4. Cleanup +wsl docker stop backup-service-test-db +wsl docker rm backup-service-test-db +``` + +### Using Docker Compose + +```bash +# Start test database +npm run db:test:up + +# Run tests +npm run test:e2e:db + +# Stop and cleanup +npm run db:test:down +``` + +### Manual Setup + +```bash +# 1. Set environment variables +export DATABASE_URL="postgresql://postgres:testpassword@localhost:5434/rwa_backup_test?schema=public" +export APP_ENV=test +export SERVICE_JWT_SECRET="test-super-secret-service-jwt-key-for-e2e-testing" +export BACKUP_ENCRYPTION_KEY="0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" +export BACKUP_ENCRYPTION_KEY_ID="test-key-v1" + +# 2. Push schema to test database +npx prisma db push + +# 3. Run tests +npm run test:e2e:db +``` + +--- + +## Test Coverage + +### Current Coverage + +| Category | Files | Tests | +|----------|-------|-------| +| Unit Tests | 10 | 37 | +| Mock E2E Tests | 1 | 21 | +| Real DB E2E Tests | 1 | 20 | +| **Total** | **12** | **78** | + +### Coverage Report + +```bash +# Generate coverage report +npm run test:cov + +# View coverage in browser +open coverage/lcov-report/index.html +``` + +### Coverage Configuration + +```json +// In package.json +"jest": { + "coveragePathIgnorePatterns": [ + "/node_modules/", + ".module.ts", + "main.ts" + ], + "collectCoverageFrom": [ + "src/**/*.ts", + "!src/**/*.module.ts", + "!src/main.ts" + ] +} +``` + +--- + +## Writing Good Tests + +### Do's + +1. **Test behavior, not implementation** + ```typescript + // Good: Tests what the method does + it('should encrypt share data before storing', async () => { + await handler.execute(command); + expect(mockEncryption.encrypt).toHaveBeenCalledWith('original-data'); + }); + + // Bad: Tests internal implementation details + it('should call private method _processData', () => { /* ... */ }); + ``` + +2. **Use descriptive test names** + ```typescript + // Good + it('should return 401 when service token is missing', () => {}); + it('should increment access count on each retrieve', () => {}); + + // Bad + it('test1', () => {}); + it('works correctly', () => {}); + ``` + +3. **Arrange-Act-Assert pattern** + ```typescript + it('should store backup share successfully', async () => { + // Arrange + const command = createStoreCommand(); + mockRepository.findByUserId.mockResolvedValue(null); + + // Act + const result = await handler.execute(command); + + // Assert + expect(result.shareId).toBeDefined(); + }); + ``` + +4. **One assertion per test (when possible)** + ```typescript + // Good: Focused tests + it('should return success true', () => { /* ... */ }); + it('should return the share ID', () => { /* ... */ }); + + // Acceptable for E2E: Multiple assertions for a workflow + it('should complete full lifecycle', () => { /* ... */ }); + ``` + +### Don'ts + +1. **Don't test external libraries** + ```typescript + // Bad: Testing that jwt.sign works + it('should sign JWT token', () => { + const token = jwt.sign({}, 'secret'); + expect(jwt.verify(token, 'secret')).toBeDefined(); + }); + ``` + +2. **Don't share state between tests** + ```typescript + // Bad: Tests depend on each other + let sharedData; + it('test 1', () => { sharedData = 'value'; }); + it('test 2', () => { expect(sharedData).toBe('value'); }); + ``` + +3. **Don't make tests slow** + ```typescript + // Bad: Real network calls + it('should fetch data', async () => { + const result = await fetch('https://api.example.com'); + }); + + // Good: Mock external services + it('should fetch data', async () => { + mockFetch.mockResolvedValue({ data: 'test' }); + }); + ``` + +--- + +## CI/CD Integration + +### GitHub Actions Example + +```yaml +# .github/workflows/test.yml +name: Test + +on: [push, pull_request] + +jobs: + unit-test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: '20' + - run: npm ci + - run: npm run test:unit + + e2e-test: + runs-on: ubuntu-latest + services: + postgres: + image: postgres:15-alpine + env: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: testpassword + POSTGRES_DB: rwa_backup_test + ports: + - 5434:5432 + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: '20' + - run: npm ci + - run: npx prisma generate + - run: npx prisma db push + env: + DATABASE_URL: postgresql://postgres:testpassword@localhost:5434/rwa_backup_test + - run: npm run test:e2e:db + env: + DATABASE_URL: postgresql://postgres:testpassword@localhost:5434/rwa_backup_test + SERVICE_JWT_SECRET: test-secret + BACKUP_ENCRYPTION_KEY: 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef + BACKUP_ENCRYPTION_KEY_ID: test-key-v1 +``` + +--- + +## Debugging Tests + +### VSCode Debug Configuration + +```json +{ + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "launch", + "name": "Debug Jest Tests", + "program": "${workspaceFolder}/node_modules/.bin/jest", + "args": [ + "--runInBand", + "--no-cache", + "${fileBasenameNoExtension}" + ], + "console": "integratedTerminal" + } + ] +} +``` + +### Running Single Test + +```bash +# Run specific test file +npm test -- backup-share.entity.spec.ts + +# Run tests matching pattern +npm test -- --testNamePattern="should store" + +# Run with verbose output +npm test -- --verbose +``` diff --git a/backend/services/backup-service/eslint.config.mjs b/backend/services/backup-service/eslint.config.mjs new file mode 100644 index 00000000..4e9f8271 --- /dev/null +++ b/backend/services/backup-service/eslint.config.mjs @@ -0,0 +1,35 @@ +// @ts-check +import eslint from '@eslint/js'; +import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended'; +import globals from 'globals'; +import tseslint from 'typescript-eslint'; + +export default tseslint.config( + { + ignores: ['eslint.config.mjs'], + }, + eslint.configs.recommended, + ...tseslint.configs.recommendedTypeChecked, + eslintPluginPrettierRecommended, + { + languageOptions: { + globals: { + ...globals.node, + ...globals.jest, + }, + sourceType: 'commonjs', + parserOptions: { + projectService: true, + tsconfigRootDir: import.meta.dirname, + }, + }, + }, + { + rules: { + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/no-floating-promises': 'warn', + '@typescript-eslint/no-unsafe-argument': 'warn', + "prettier/prettier": ["error", { endOfLine: "auto" }], + }, + }, +); diff --git a/backend/services/backup-service/nest-cli.json b/backend/services/backup-service/nest-cli.json new file mode 100644 index 00000000..f9aa683b --- /dev/null +++ b/backend/services/backup-service/nest-cli.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://json.schemastore.org/nest-cli", + "collection": "@nestjs/schematics", + "sourceRoot": "src", + "compilerOptions": { + "deleteOutDir": true + } +} diff --git a/backend/services/backup-service/package-lock.json b/backend/services/backup-service/package-lock.json new file mode 100644 index 00000000..b27e60b3 --- /dev/null +++ b/backend/services/backup-service/package-lock.json @@ -0,0 +1,11226 @@ +{ + "name": "backup-service", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "backup-service", + "version": "1.0.0", + "license": "UNLICENSED", + "dependencies": { + "@nestjs/common": "^11.0.1", + "@nestjs/config": "^4.0.2", + "@nestjs/core": "^11.0.1", + "@nestjs/platform-express": "^11.0.1", + "@prisma/adapter-pg": "^7.0.1", + "@prisma/client": "^7.0.1", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.3", + "dotenv": "^17.2.3", + "jsonwebtoken": "^9.0.2", + "reflect-metadata": "^0.2.2", + "rxjs": "^7.8.1", + "uuid": "^13.0.0" + }, + "devDependencies": { + "@eslint/eslintrc": "^3.2.0", + "@eslint/js": "^9.18.0", + "@nestjs/cli": "^11.0.0", + "@nestjs/schematics": "^11.0.0", + "@nestjs/testing": "^11.0.1", + "@types/express": "^5.0.0", + "@types/jest": "^30.0.0", + "@types/jsonwebtoken": "^9.0.10", + "@types/node": "^22.10.7", + "@types/pg": "^8.15.6", + "@types/supertest": "^6.0.2", + "@types/uuid": "^10.0.0", + "dotenv-cli": "^11.0.0", + "eslint": "^9.18.0", + "eslint-config-prettier": "^10.0.1", + "eslint-plugin-prettier": "^5.2.2", + "globals": "^16.0.0", + "jest": "^30.0.0", + "pg": "^8.16.3", + "prettier": "^3.4.2", + "prisma": "^7.0.1", + "source-map-support": "^0.5.21", + "supertest": "^7.0.0", + "ts-jest": "^29.2.5", + "ts-loader": "^9.5.2", + "ts-node": "^10.9.2", + "tsconfig-paths": "^4.2.0", + "typescript": "^5.7.3", + "typescript-eslint": "^8.20.0" + } + }, + "node_modules/@angular-devkit/core": { + "version": "19.2.19", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-19.2.19.tgz", + "integrity": "sha512-JbLL+4IMLMBgjLZlnPG4lYDfz4zGrJ/s6Aoon321NJKuw1Kb1k5KpFu9dUY0BqLIe8xPQ2UJBpI+xXdK5MXMHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "8.17.1", + "ajv-formats": "3.0.1", + "jsonc-parser": "3.3.1", + "picomatch": "4.0.2", + "rxjs": "7.8.1", + "source-map": "0.7.4" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^4.0.0" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/core/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@angular-devkit/core/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/@angular-devkit/core/node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@angular-devkit/schematics": { + "version": "19.2.19", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-19.2.19.tgz", + "integrity": "sha512-J4Jarr0SohdrHcb40gTL4wGPCQ952IMWF1G/MSAQfBAPvA9ZKApYhpxcY7PmehVePve+ujpus1dGsJ7dPxz8Kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "19.2.19", + "jsonc-parser": "3.3.1", + "magic-string": "0.30.17", + "ora": "5.4.1", + "rxjs": "7.8.1" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular-devkit/schematics-cli": { + "version": "19.2.19", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics-cli/-/schematics-cli-19.2.19.tgz", + "integrity": "sha512-7q9UY6HK6sccL9F3cqGRUwKhM7b/XfD2YcVaZ2WD7VMaRlRm85v6mRjSrfKIAwxcQU0UK27kMc79NIIqaHjzxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "19.2.19", + "@angular-devkit/schematics": "19.2.19", + "@inquirer/prompts": "7.3.2", + "ansi-colors": "4.1.3", + "symbol-observable": "4.0.0", + "yargs-parser": "21.1.1" + }, + "bin": { + "schematics": "bin/schematics.js" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular-devkit/schematics-cli/node_modules/@inquirer/prompts": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.3.2.tgz", + "integrity": "sha512-G1ytyOoHh5BphmEBxSwALin3n1KGNYB6yImbICcRQdzXfOGbuJ9Jske/Of5Sebk339NSGGNfUshnzK8YWkTPsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/checkbox": "^4.1.2", + "@inquirer/confirm": "^5.1.6", + "@inquirer/editor": "^4.2.7", + "@inquirer/expand": "^4.0.9", + "@inquirer/input": "^4.1.6", + "@inquirer/number": "^3.0.9", + "@inquirer/password": "^4.0.9", + "@inquirer/rawlist": "^4.0.9", + "@inquirer/search": "^3.0.9", + "@inquirer/select": "^4.0.9" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/schematics/node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", + "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", + "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", + "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.5" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz", + "integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz", + "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz", + "integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", + "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.5", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@borewit/text-codec": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@borewit/text-codec/-/text-codec-0.1.1.tgz", + "integrity": "sha512-5L/uBxmjaCIX5h8Z+uu+kA9BQLkc/Wl06UGR5ajNRxu+/XjonB5i8JpgFMrPj3LXTCPA0pv8yxUvbUi+QthGGA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@chevrotain/cst-dts-gen": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-10.5.0.tgz", + "integrity": "sha512-lhmC/FyqQ2o7pGK4Om+hzuDrm9rhFYIJ/AXoQBeongmn870Xeb0L6oGEiuR8nohFNL5sMaQEJWCxr1oIVIVXrw==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/gast": "10.5.0", + "@chevrotain/types": "10.5.0", + "lodash": "4.17.21" + } + }, + "node_modules/@chevrotain/gast": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/@chevrotain/gast/-/gast-10.5.0.tgz", + "integrity": "sha512-pXdMJ9XeDAbgOWKuD1Fldz4ieCs6+nLNmyVhe2gZVqoO7v8HXuHYs5OV2EzUtbuai37TlOAQHrTDvxMnvMJz3A==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/types": "10.5.0", + "lodash": "4.17.21" + } + }, + "node_modules/@chevrotain/types": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-10.5.0.tgz", + "integrity": "sha512-f1MAia0x/pAVPWH/T73BJVyO2XU5tI4/iE7cnxb7tqdNTNhQI3Uq3XkqcoteTmD4t1aM0LbHCJOhgIDn07kl2A==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@chevrotain/utils": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/@chevrotain/utils/-/utils-10.5.0.tgz", + "integrity": "sha512-hBzuU5+JjB2cqNZyszkDHZgOSrUUT8V3dhgRl8Q9Gp6dAj/H5+KILGjbhDpc3Iy9qmqlm/akuOI2ut9VUtzJxQ==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@electric-sql/pglite": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@electric-sql/pglite/-/pglite-0.3.2.tgz", + "integrity": "sha512-zfWWa+V2ViDCY/cmUfRqeWY1yLto+EpxjXnZzenB1TyxsTiXaTWeZFIZw6mac52BsuQm0RjCnisjBtdBaXOI6w==", + "devOptional": true, + "license": "Apache-2.0", + "peer": true + }, + "node_modules/@electric-sql/pglite-socket": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@electric-sql/pglite-socket/-/pglite-socket-0.0.6.tgz", + "integrity": "sha512-6RjmgzphIHIBA4NrMGJsjNWK4pu+bCWJlEWlwcxFTVY3WT86dFpKwbZaGWZV6C5Rd7sCk1Z0CI76QEfukLAUXw==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "pglite-server": "dist/scripts/server.js" + }, + "peerDependencies": { + "@electric-sql/pglite": "0.3.2" + } + }, + "node_modules/@electric-sql/pglite-tools": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/@electric-sql/pglite-tools/-/pglite-tools-0.2.7.tgz", + "integrity": "sha512-9dAccClqxx4cZB+Ar9B+FZ5WgxDc/Xvl9DPrTWv+dYTf0YNubLzi4wHHRGRGhrJv15XwnyKcGOZAP1VXSneSUg==", + "devOptional": true, + "license": "Apache-2.0", + "peerDependencies": { + "@electric-sql/pglite": "0.3.2" + } + }, + "node_modules/@emnapi/core": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.7.1.tgz", + "integrity": "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.1.0", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.7.1.tgz", + "integrity": "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", + "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz", + "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.1.tgz", + "integrity": "sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@hono/node-server": { + "version": "1.14.2", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.14.2.tgz", + "integrity": "sha512-GHjpOeHYbr9d1vkID2sNUYkl5IxumyhDrUJB7wBp7jvqYwPFt+oNKsAPBRcdSbV7kIrXhouLE199ks1QcK4r7A==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@inquirer/ansi": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.2.tgz", + "integrity": "sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/checkbox": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.3.2.tgz", + "integrity": "sha512-VXukHf0RR1doGe6Sm4F0Em7SWYLTHSsbGfJdS9Ja2bX5/D5uwVOEjr07cncLROdBvmnvCATYEWlHqYmXv2IlQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/confirm": { + "version": "5.1.21", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.21.tgz", + "integrity": "sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core": { + "version": "10.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.3.2.tgz", + "integrity": "sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "cli-width": "^4.1.0", + "mute-stream": "^2.0.0", + "signal-exit": "^4.1.0", + "wrap-ansi": "^6.2.0", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/editor": { + "version": "4.2.23", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.23.tgz", + "integrity": "sha512-aLSROkEwirotxZ1pBaP8tugXRFCxW94gwrQLxXfrZsKkfjOYC1aRvAZuhpJOb5cu4IBTJdsCigUlf2iCOu4ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/external-editor": "^1.0.3", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/expand": { + "version": "4.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.23.tgz", + "integrity": "sha512-nRzdOyFYnpeYTTR2qFwEVmIWypzdAx/sIkCMeTNTcflFOovfqUk+HcFhQQVBftAh9gmGrpFj6QcGEqrDMDOiew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/external-editor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.3.tgz", + "integrity": "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chardet": "^2.1.1", + "iconv-lite": "^0.7.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/figures": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.15.tgz", + "integrity": "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/input": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.3.1.tgz", + "integrity": "sha512-kN0pAM4yPrLjJ1XJBjDxyfDduXOuQHrBB8aLDMueuwUGn+vNpF7Gq7TvyVxx8u4SHlFFj4trmj+a2cbpG4Jn1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/number": { + "version": "3.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.23.tgz", + "integrity": "sha512-5Smv0OK7K0KUzUfYUXDXQc9jrf8OHo4ktlEayFlelCjwMXz0299Y8OrI+lj7i4gCBY15UObk76q0QtxjzFcFcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/password": { + "version": "4.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.23.tgz", + "integrity": "sha512-zREJHjhT5vJBMZX/IUbyI9zVtVfOLiTO66MrF/3GFZYZ7T4YILW5MSkEYHceSii/KtRk+4i3RE7E1CUXA2jHcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/prompts": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.10.1.tgz", + "integrity": "sha512-Dx/y9bCQcXLI5ooQ5KyvA4FTgeo2jYj/7plWfV5Ak5wDPKQZgudKez2ixyfz7tKXzcJciTxqLeK7R9HItwiByg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/checkbox": "^4.3.2", + "@inquirer/confirm": "^5.1.21", + "@inquirer/editor": "^4.2.23", + "@inquirer/expand": "^4.0.23", + "@inquirer/input": "^4.3.1", + "@inquirer/number": "^3.0.23", + "@inquirer/password": "^4.0.23", + "@inquirer/rawlist": "^4.1.11", + "@inquirer/search": "^3.2.2", + "@inquirer/select": "^4.4.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/rawlist": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.1.11.tgz", + "integrity": "sha512-+LLQB8XGr3I5LZN/GuAHo+GpDJegQwuPARLChlMICNdwW7OwV2izlCSCxN6cqpL0sMXmbKbFcItJgdQq5EBXTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/search": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.2.2.tgz", + "integrity": "sha512-p2bvRfENXCZdWF/U2BXvnSI9h+tuA8iNqtUKb9UWbmLYCRQxd8WkvwWvYn+3NgYaNwdUkHytJMGG4MMLucI1kA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/select": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.4.2.tgz", + "integrity": "sha512-l4xMuJo55MAe+N7Qr4rX90vypFwCajSakx59qe/tMaC1aEHWLyw68wF4o0A4SLAY4E0nd+Vt+EyskeDIqu1M6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/type": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.10.tgz", + "integrity": "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@isaacs/balanced-match": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", + "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@isaacs/brace-expansion": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz", + "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@isaacs/balanced-match": "^4.0.1" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-30.2.0.tgz", + "integrity": "sha512-+O1ifRjkvYIkBqASKWgLxrpEhQAAE7hY77ALLUufSk5717KfOShg6IbqLmdsLMPdUiFvA2kTs0R7YZy+l0IzZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.2.0", + "@types/node": "*", + "chalk": "^4.1.2", + "jest-message-util": "30.2.0", + "jest-util": "30.2.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/core": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-30.2.0.tgz", + "integrity": "sha512-03W6IhuhjqTlpzh/ojut/pDB2LPRygyWX8ExpgHtQA8H/3K7+1vKmcINx5UzeOX1se6YEsBsOHQ1CRzf3fOwTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "30.2.0", + "@jest/pattern": "30.0.1", + "@jest/reporters": "30.2.0", + "@jest/test-result": "30.2.0", + "@jest/transform": "30.2.0", + "@jest/types": "30.2.0", + "@types/node": "*", + "ansi-escapes": "^4.3.2", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "exit-x": "^0.2.2", + "graceful-fs": "^4.2.11", + "jest-changed-files": "30.2.0", + "jest-config": "30.2.0", + "jest-haste-map": "30.2.0", + "jest-message-util": "30.2.0", + "jest-regex-util": "30.0.1", + "jest-resolve": "30.2.0", + "jest-resolve-dependencies": "30.2.0", + "jest-runner": "30.2.0", + "jest-runtime": "30.2.0", + "jest-snapshot": "30.2.0", + "jest-util": "30.2.0", + "jest-validate": "30.2.0", + "jest-watcher": "30.2.0", + "micromatch": "^4.0.8", + "pretty-format": "30.2.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/diff-sequences": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.0.1.tgz", + "integrity": "sha512-n5H8QLDJ47QqbCNn5SuFjCRDrOLEZ0h8vAHCK5RL9Ls7Xa8AQLa/YxAc9UjFqoEDM48muwtBGjtMY5cr0PLDCw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/environment": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.2.0.tgz", + "integrity": "sha512-/QPTL7OBJQ5ac09UDRa3EQes4gt1FTEG/8jZ/4v5IVzx+Cv7dLxlVIvfvSVRiiX2drWyXeBjkMSR8hvOWSog5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "30.2.0", + "@jest/types": "30.2.0", + "@types/node": "*", + "jest-mock": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-30.2.0.tgz", + "integrity": "sha512-V9yxQK5erfzx99Sf+7LbhBwNWEZ9eZay8qQ9+JSC0TrMR1pMDHLMY+BnVPacWU6Jamrh252/IKo4F1Xn/zfiqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "30.2.0", + "jest-snapshot": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.2.0.tgz", + "integrity": "sha512-1JnRfhqpD8HGpOmQp180Fo9Zt69zNtC+9lR+kT7NVL05tNXIi+QC8Csz7lfidMoVLPD3FnOtcmp0CEFnxExGEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.2.0.tgz", + "integrity": "sha512-HI3tRLjRxAbBy0VO8dqqm7Hb2mIa8d5bg/NJkyQcOk7V118ObQML8RC5luTF/Zsg4474a+gDvhce7eTnP4GhYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.2.0", + "@sinonjs/fake-timers": "^13.0.0", + "@types/node": "*", + "jest-message-util": "30.2.0", + "jest-mock": "30.2.0", + "jest-util": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/get-type": { + "version": "30.1.0", + "resolved": "https://registry.npmjs.org/@jest/get-type/-/get-type-30.1.0.tgz", + "integrity": "sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-30.2.0.tgz", + "integrity": "sha512-b63wmnKPaK+6ZZfpYhz9K61oybvbI1aMcIs80++JI1O1rR1vaxHUCNqo3ITu6NU0d4V34yZFoHMn/uoKr/Rwfw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.2.0", + "@jest/expect": "30.2.0", + "@jest/types": "30.2.0", + "jest-mock": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/pattern": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.0.1.tgz", + "integrity": "sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-regex-util": "30.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-30.2.0.tgz", + "integrity": "sha512-DRyW6baWPqKMa9CzeiBjHwjd8XeAyco2Vt8XbcLFjiwCOEKOvy82GJ8QQnJE9ofsxCMPjH4MfH8fCWIHHDKpAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "30.2.0", + "@jest/test-result": "30.2.0", + "@jest/transform": "30.2.0", + "@jest/types": "30.2.0", + "@jridgewell/trace-mapping": "^0.3.25", + "@types/node": "*", + "chalk": "^4.1.2", + "collect-v8-coverage": "^1.0.2", + "exit-x": "^0.2.2", + "glob": "^10.3.10", + "graceful-fs": "^4.2.11", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^5.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "30.2.0", + "jest-util": "30.2.0", + "jest-worker": "30.2.0", + "slash": "^3.0.0", + "string-length": "^4.0.2", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/reporters/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@jest/reporters/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@jest/reporters/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@jest/reporters/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@jest/reporters/node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/snapshot-utils": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/snapshot-utils/-/snapshot-utils-30.2.0.tgz", + "integrity": "sha512-0aVxM3RH6DaiLcjj/b0KrIBZhSX1373Xci4l3cW5xiUWPctZ59zQ7jj4rqcJQ/Z8JuN/4wX3FpJSa3RssVvCug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.2.0", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "natural-compare": "^1.4.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-30.0.1.tgz", + "integrity": "sha512-MIRWMUUR3sdbP36oyNyhbThLHyJ2eEDClPCiHVbrYAe5g3CHRArIVpBw7cdSB5fr+ofSfIb2Tnsw8iEHL0PYQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "callsites": "^3.1.0", + "graceful-fs": "^4.2.11" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-30.2.0.tgz", + "integrity": "sha512-RF+Z+0CCHkARz5HT9mcQCBulb1wgCP3FBvl9VFokMX27acKphwyQsNuWH3c+ojd1LeWBLoTYoxF0zm6S/66mjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "30.2.0", + "@jest/types": "30.2.0", + "@types/istanbul-lib-coverage": "^2.0.6", + "collect-v8-coverage": "^1.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-30.2.0.tgz", + "integrity": "sha512-wXKgU/lk8fKXMu/l5Hog1R61bL4q5GCdT6OJvdAFz1P+QrpoFuLU68eoKuVc4RbrTtNnTL5FByhWdLgOPSph+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "30.2.0", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.2.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.2.0.tgz", + "integrity": "sha512-XsauDV82o5qXbhalKxD7p4TZYYdwcaEXC77PPD2HixEFF+6YGppjrAAQurTl2ECWcEomHBMMNS9AH3kcCFx8jA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", + "@jest/types": "30.2.0", + "@jridgewell/trace-mapping": "^0.3.25", + "babel-plugin-istanbul": "^7.0.1", + "chalk": "^4.1.2", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.2.0", + "jest-regex-util": "30.0.1", + "jest-util": "30.2.0", + "micromatch": "^4.0.8", + "pirates": "^4.0.7", + "slash": "^3.0.0", + "write-file-atomic": "^5.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/types": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.2.0.tgz", + "integrity": "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@lukeed/csprng": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@lukeed/csprng/-/csprng-1.1.0.tgz", + "integrity": "sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@mrleebo/prisma-ast": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/@mrleebo/prisma-ast/-/prisma-ast-0.12.1.tgz", + "integrity": "sha512-JwqeCQ1U3fvccttHZq7Tk0m/TMC6WcFAQZdukypW3AzlJYKYTGNVd1ANU2GuhKnv4UQuOFj3oAl0LLG/gxFN1w==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "chevrotain": "^10.5.0", + "lilconfig": "^2.1.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", + "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@tybys/wasm-util": "^0.10.0" + } + }, + "node_modules/@nestjs/cli": { + "version": "11.0.14", + "resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-11.0.14.tgz", + "integrity": "sha512-YwP03zb5VETTwelXU+AIzMVbEZKk/uxJL+z9pw0mdG9ogAtqZ6/mpmIM4nEq/NU8D0a7CBRLcMYUmWW/55pfqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "19.2.19", + "@angular-devkit/schematics": "19.2.19", + "@angular-devkit/schematics-cli": "19.2.19", + "@inquirer/prompts": "7.10.1", + "@nestjs/schematics": "^11.0.1", + "ansis": "4.2.0", + "chokidar": "4.0.3", + "cli-table3": "0.6.5", + "commander": "4.1.1", + "fork-ts-checker-webpack-plugin": "9.1.0", + "glob": "13.0.0", + "node-emoji": "1.11.0", + "ora": "5.4.1", + "tsconfig-paths": "4.2.0", + "tsconfig-paths-webpack-plugin": "4.2.0", + "typescript": "5.9.3", + "webpack": "5.103.0", + "webpack-node-externals": "3.0.0" + }, + "bin": { + "nest": "bin/nest.js" + }, + "engines": { + "node": ">= 20.11" + }, + "peerDependencies": { + "@swc/cli": "^0.1.62 || ^0.3.0 || ^0.4.0 || ^0.5.0 || ^0.6.0 || ^0.7.0", + "@swc/core": "^1.3.62" + }, + "peerDependenciesMeta": { + "@swc/cli": { + "optional": true + }, + "@swc/core": { + "optional": true + } + } + }, + "node_modules/@nestjs/common": { + "version": "11.1.9", + "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.1.9.tgz", + "integrity": "sha512-zDntUTReRbAThIfSp3dQZ9kKqI+LjgLp5YZN5c1bgNRDuoeLySAoZg46Bg1a+uV8TMgIRziHocglKGNzr6l+bQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "file-type": "21.1.0", + "iterare": "1.2.1", + "load-esm": "1.0.3", + "tslib": "2.8.1", + "uid": "2.0.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "class-transformer": ">=0.4.1", + "class-validator": ">=0.13.2", + "reflect-metadata": "^0.1.12 || ^0.2.0", + "rxjs": "^7.1.0" + }, + "peerDependenciesMeta": { + "class-transformer": { + "optional": true + }, + "class-validator": { + "optional": true + } + } + }, + "node_modules/@nestjs/config": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@nestjs/config/-/config-4.0.2.tgz", + "integrity": "sha512-McMW6EXtpc8+CwTUwFdg6h7dYcBUpH5iUILCclAsa+MbCEvC9ZKu4dCHRlJqALuhjLw97pbQu62l4+wRwGeZqA==", + "license": "MIT", + "dependencies": { + "dotenv": "16.4.7", + "dotenv-expand": "12.0.1", + "lodash": "4.17.21" + }, + "peerDependencies": { + "@nestjs/common": "^10.0.0 || ^11.0.0", + "rxjs": "^7.1.0" + } + }, + "node_modules/@nestjs/config/node_modules/dotenv": { + "version": "16.4.7", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", + "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/@nestjs/core": { + "version": "11.1.9", + "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-11.1.9.tgz", + "integrity": "sha512-a00B0BM4X+9z+t3UxJqIZlemIwCQdYoPKrMcM+ky4z3pkqqG1eTWexjs+YXpGObnLnjtMPVKWlcZHp3adDYvUw==", + "hasInstallScript": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@nuxt/opencollective": "0.4.1", + "fast-safe-stringify": "2.1.1", + "iterare": "1.2.1", + "path-to-regexp": "8.3.0", + "tslib": "2.8.1", + "uid": "2.0.2" + }, + "engines": { + "node": ">= 20" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "@nestjs/common": "^11.0.0", + "@nestjs/microservices": "^11.0.0", + "@nestjs/platform-express": "^11.0.0", + "@nestjs/websockets": "^11.0.0", + "reflect-metadata": "^0.1.12 || ^0.2.0", + "rxjs": "^7.1.0" + }, + "peerDependenciesMeta": { + "@nestjs/microservices": { + "optional": true + }, + "@nestjs/platform-express": { + "optional": true + }, + "@nestjs/websockets": { + "optional": true + } + } + }, + "node_modules/@nestjs/platform-express": { + "version": "11.1.9", + "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.1.9.tgz", + "integrity": "sha512-GVd3+0lO0mJq2m1kl9hDDnVrX3Nd4oH3oDfklz0pZEVEVS0KVSp63ufHq2Lu9cyPdSBuelJr9iPm2QQ1yX+Kmw==", + "license": "MIT", + "peer": true, + "dependencies": { + "cors": "2.8.5", + "express": "5.1.0", + "multer": "2.0.2", + "path-to-regexp": "8.3.0", + "tslib": "2.8.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "@nestjs/common": "^11.0.0", + "@nestjs/core": "^11.0.0" + } + }, + "node_modules/@nestjs/schematics": { + "version": "11.0.9", + "resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-11.0.9.tgz", + "integrity": "sha512-0NfPbPlEaGwIT8/TCThxLzrlz3yzDNkfRNpbL7FiplKq3w4qXpJg0JYwqgMEJnLQZm3L/L/5XjoyfJHUO3qX9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "19.2.17", + "@angular-devkit/schematics": "19.2.17", + "comment-json": "4.4.1", + "jsonc-parser": "3.3.1", + "pluralize": "8.0.0" + }, + "peerDependencies": { + "typescript": ">=4.8.2" + } + }, + "node_modules/@nestjs/schematics/node_modules/@angular-devkit/core": { + "version": "19.2.17", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-19.2.17.tgz", + "integrity": "sha512-Ah008x2RJkd0F+NLKqIpA34/vUGwjlprRCkvddjDopAWRzYn6xCkz1Tqwuhn0nR1Dy47wTLKYD999TYl5ONOAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "8.17.1", + "ajv-formats": "3.0.1", + "jsonc-parser": "3.3.1", + "picomatch": "4.0.2", + "rxjs": "7.8.1", + "source-map": "0.7.4" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^4.0.0" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@nestjs/schematics/node_modules/@angular-devkit/schematics": { + "version": "19.2.17", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-19.2.17.tgz", + "integrity": "sha512-ADfbaBsrG8mBF6Mfs+crKA/2ykB8AJI50Cv9tKmZfwcUcyAdmTr+vVvhsBCfvUAEokigSsgqgpYxfkJVxhJYeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "19.2.17", + "jsonc-parser": "3.3.1", + "magic-string": "0.30.17", + "ora": "5.4.1", + "rxjs": "7.8.1" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@nestjs/schematics/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@nestjs/schematics/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/@nestjs/schematics/node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@nestjs/testing": { + "version": "11.1.9", + "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-11.1.9.tgz", + "integrity": "sha512-UFxerBDdb0RUNxQNj25pvkvNE7/vxKhXYWBt3QuwBFnYISzRIzhVlyIqLfoV5YI3zV0m0Nn4QAn1KM0zzwfEng==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "@nestjs/common": "^11.0.0", + "@nestjs/core": "^11.0.0", + "@nestjs/microservices": "^11.0.0", + "@nestjs/platform-express": "^11.0.0" + }, + "peerDependenciesMeta": { + "@nestjs/microservices": { + "optional": true + }, + "@nestjs/platform-express": { + "optional": true + } + } + }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@nuxt/opencollective": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@nuxt/opencollective/-/opencollective-0.4.1.tgz", + "integrity": "sha512-GXD3wy50qYbxCJ652bDrDzgMr3NFEkIS374+IgFQKkCvk9yiYcLvX2XDYr7UyQxf4wK0e+yqDYRubZ0DtOxnmQ==", + "license": "MIT", + "dependencies": { + "consola": "^3.2.3" + }, + "bin": { + "opencollective": "bin/opencollective.js" + }, + "engines": { + "node": "^14.18.0 || >=16.10.0", + "npm": ">=5.10.0" + } + }, + "node_modules/@paralleldrive/cuid2": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.3.1.tgz", + "integrity": "sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "^1.1.5" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@pkgr/core": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz", + "integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/pkgr" + } + }, + "node_modules/@prisma/adapter-pg": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@prisma/adapter-pg/-/adapter-pg-7.0.1.tgz", + "integrity": "sha512-01GpPPhLMoDMF4ipgfZz0L87fla/TV/PBQcmHy+9vV1ml6gUoqF8dUIRNI5Yf2YKpOwzQg9sn8C7dYD1Yio9Ug==", + "license": "Apache-2.0", + "dependencies": { + "@prisma/driver-adapter-utils": "7.0.1", + "pg": "^8.16.3", + "postgres-array": "3.0.4" + } + }, + "node_modules/@prisma/adapter-pg/node_modules/postgres-array": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-3.0.4.tgz", + "integrity": "sha512-nAUSGfSDGOaOAEGwqsRY27GPOea7CNipJPOA7lPbdEpx5Kg3qzdP0AaWC5MlhTWV9s4hFX39nomVZ+C4tnGOJQ==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/@prisma/client": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-7.0.1.tgz", + "integrity": "sha512-O74T6xcfaGAq5gXwCAvfTLvI6fmC3and2g5yLRMkNjri1K8mSpEgclDNuUWs9xj5AwNEMQ88NeD3asI+sovm1g==", + "license": "Apache-2.0", + "dependencies": { + "@prisma/client-runtime-utils": "7.0.1" + }, + "engines": { + "node": "^20.19 || ^22.12 || >=24.0" + }, + "peerDependencies": { + "prisma": "*", + "typescript": ">=5.4.0" + }, + "peerDependenciesMeta": { + "prisma": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/@prisma/client-runtime-utils": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@prisma/client-runtime-utils/-/client-runtime-utils-7.0.1.tgz", + "integrity": "sha512-R26BVX9D/iw4toUmZKZf3jniM/9pMGHHdZN5LVP2L7HNiCQKNQQx/9LuMtjepbgRqSqQO3oHN0yzojHLnKTGEw==", + "license": "Apache-2.0" + }, + "node_modules/@prisma/config": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@prisma/config/-/config-7.0.1.tgz", + "integrity": "sha512-MacIjXdo+hNKxPvtMzDXykIIc8HCRWoyjQ2nguJTFqLDzJBD5L6QRaANGTLOqbGtJ3sFvLRmfXhrFg3pWoK1BA==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "c12": "3.1.0", + "deepmerge-ts": "7.1.5", + "effect": "3.18.4", + "empathic": "2.0.0" + } + }, + "node_modules/@prisma/debug": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-7.0.1.tgz", + "integrity": "sha512-5+25XokVeAK2Z2C9W457AFw7Hk032Q3QI3G58KYKXPlpgxy+9FvV1+S1jqfJ2d4Nmq9LP/uACrM6OVhpJMSr8w==", + "license": "Apache-2.0" + }, + "node_modules/@prisma/dev": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@prisma/dev/-/dev-0.13.0.tgz", + "integrity": "sha512-QMmF6zFeUF78yv1HYbHvod83AQnl7u6NtKyDhTRZOJup3h1icWs8R7RUVxBJZvM2tBXNAMpLQYYM/8kPlOPegA==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "@electric-sql/pglite": "0.3.2", + "@electric-sql/pglite-socket": "0.0.6", + "@electric-sql/pglite-tools": "0.2.7", + "@hono/node-server": "1.14.2", + "@mrleebo/prisma-ast": "0.12.1", + "@prisma/get-platform": "6.8.2", + "@prisma/query-plan-executor": "6.18.0", + "foreground-child": "3.3.1", + "get-port-please": "3.1.2", + "hono": "4.7.10", + "http-status-codes": "2.3.0", + "pathe": "2.0.3", + "proper-lockfile": "4.1.2", + "remeda": "2.21.3", + "std-env": "3.9.0", + "valibot": "1.1.0", + "zeptomatch": "2.0.2" + } + }, + "node_modules/@prisma/driver-adapter-utils": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@prisma/driver-adapter-utils/-/driver-adapter-utils-7.0.1.tgz", + "integrity": "sha512-sBbxm/yysHLLF2iMAB+qcX/nn3WFgsiC4DQNz0uM6BwGSIs8lIvgo0u8nR9nxe5gvFgKiIH8f4z2fgOEMeXc8w==", + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.0.1" + } + }, + "node_modules/@prisma/engines": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-7.0.1.tgz", + "integrity": "sha512-f+D/vdKeImqUHysd5Bgv8LQ1whl4sbLepHyYMQQMK61cp4WjwJVryophleLUrfEJRpBLGTBI/7fnLVENxxMFPQ==", + "devOptional": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.0.1", + "@prisma/engines-version": "7.1.0-2.f09f2815f091dbba658cdcd2264306d88bb5bda6", + "@prisma/fetch-engine": "7.0.1", + "@prisma/get-platform": "7.0.1" + } + }, + "node_modules/@prisma/engines-version": { + "version": "7.1.0-2.f09f2815f091dbba658cdcd2264306d88bb5bda6", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-7.1.0-2.f09f2815f091dbba658cdcd2264306d88bb5bda6.tgz", + "integrity": "sha512-RA7pShKvijHib4USRB3YuLTQamHKJPkTRDc45AwxfahUQngiGVMlIj4ix4emUxkrum4o/jwn82WIwlG57EtgiQ==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/engines/node_modules/@prisma/get-platform": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.0.1.tgz", + "integrity": "sha512-DrsGnZOsF7PlAE7UtqmJenWti87RQtg7v9qW9alS71Pj0P6ZQV0RuzRQaql9dCWoo6qKAaF5U/L4kI826MmiZg==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.0.1" + } + }, + "node_modules/@prisma/fetch-engine": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-7.0.1.tgz", + "integrity": "sha512-5DnSairYIYU7dcv/9pb1KCwIRHZfhVOd34855d01lUI5QdF9rdCkMywPQbBM67YP7iCgQoEZO0/COtOMpR4i9A==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.0.1", + "@prisma/engines-version": "7.1.0-2.f09f2815f091dbba658cdcd2264306d88bb5bda6", + "@prisma/get-platform": "7.0.1" + } + }, + "node_modules/@prisma/fetch-engine/node_modules/@prisma/get-platform": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.0.1.tgz", + "integrity": "sha512-DrsGnZOsF7PlAE7UtqmJenWti87RQtg7v9qW9alS71Pj0P6ZQV0RuzRQaql9dCWoo6qKAaF5U/L4kI826MmiZg==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.0.1" + } + }, + "node_modules/@prisma/get-platform": { + "version": "6.8.2", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.8.2.tgz", + "integrity": "sha512-vXSxyUgX3vm1Q70QwzwkjeYfRryIvKno1SXbIqwSptKwqKzskINnDUcx85oX+ys6ooN2ATGSD0xN2UTfg6Zcow==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "6.8.2" + } + }, + "node_modules/@prisma/get-platform/node_modules/@prisma/debug": { + "version": "6.8.2", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.8.2.tgz", + "integrity": "sha512-4muBSSUwJJ9BYth5N8tqts8JtiLT8QI/RSAzEogwEfpbYGFo9mYsInsVo8dqXdPO2+Rm5OG5q0qWDDE3nyUbVg==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/query-plan-executor": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/@prisma/query-plan-executor/-/query-plan-executor-6.18.0.tgz", + "integrity": "sha512-jZ8cfzFgL0jReE1R10gT8JLHtQxjWYLiQ//wHmVYZ2rVkFHoh0DT8IXsxcKcFlfKN7ak7k6j0XMNn2xVNyr5cA==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/studio-core": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/@prisma/studio-core/-/studio-core-0.8.2.tgz", + "integrity": "sha512-/iAEWEUpTja+7gVMu1LtR2pPlvDmveAwMHdTWbDeGlT7yiv0ZTCPpmeAGdq/Y9aJ9Zj1cEGBXGRbmmNPj022PQ==", + "devOptional": true, + "license": "UNLICENSED", + "peerDependencies": { + "@types/react": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.34.41", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.41.tgz", + "integrity": "sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "13.0.5", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-13.0.5.tgz", + "integrity": "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.1" + } + }, + "node_modules/@standard-schema/spec": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", + "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@tokenizer/inflate": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.3.1.tgz", + "integrity": "sha512-4oeoZEBQdLdt5WmP/hx1KZ6D3/Oid/0cUb2nk4F0pTDAWy+KCH3/EnAkZF/bvckWo8I33EqBm01lIPgmgc8rCA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.1", + "fflate": "^0.8.2", + "token-types": "^6.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@tokenizer/token": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", + "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", + "license": "MIT" + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", + "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cookiejar": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz", + "integrity": "sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/eslint": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/express": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.5.tgz", + "integrity": "sha512-LuIQOcb6UmnF7C1PCFmEU1u2hmiHL43fgFQX67sN3H4Z+0Yk0Neo++mFsBjhOAuLzvlQeqAAkeDOZrJs9rzumQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "^1" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.0.tgz", + "integrity": "sha512-jnHMsrd0Mwa9Cf4IdOzbz543y4XJepXrbia2T4b6+spXC2We3t1y6K44D3mR8XMFSXMCf3/l7rCgddfx7UNVBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "30.0.0", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-30.0.0.tgz", + "integrity": "sha512-XTYugzhuwqWjws0CVz8QpM36+T+Dz5mTEBKhNs/esGLnCIlGdRy+Dq78NRjd7ls7r8BC8ZRMOrKlkO1hU0JOwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^30.0.0", + "pretty-format": "^30.0.0" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.10", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", + "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ms": "*", + "@types/node": "*" + } + }, + "node_modules/@types/methods": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", + "integrity": "sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.19.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.1.tgz", + "integrity": "sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/pg": { + "version": "8.15.6", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.15.6.tgz", + "integrity": "sha512-NoaMtzhxOrubeL/7UZuNTrejB4MPAJ0RpxZqXQf2qXuVlTPuG6Y8p4u9dKRaue4yjmC7ZhzVO2/Yyyn25znrPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "pg-protocol": "*", + "pg-types": "^2.2.0" + } + }, + "node_modules/@types/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz", + "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", + "devOptional": true, + "license": "MIT", + "peer": true, + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.10", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz", + "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "<1" + } + }, + "node_modules/@types/serve-static/node_modules/@types/send": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz", + "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/superagent": { + "version": "8.1.9", + "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-8.1.9.tgz", + "integrity": "sha512-pTVjI73witn+9ILmoJdajHGW2jkSaOzhiFYF1Rd3EQ94kymLqB9PjD9ISg7WaALC7+dCHT0FGe9T2LktLq/3GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/cookiejar": "^2.1.5", + "@types/methods": "^1.1.4", + "@types/node": "*", + "form-data": "^4.0.0" + } + }, + "node_modules/@types/supertest": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/supertest/-/supertest-6.0.3.tgz", + "integrity": "sha512-8WzXq62EXFhJ7QsH3Ocb/iKQ/Ty9ZVWnVzoTKc9tyyFRRF3a74Tk2+TLFgaFFw364Ere+npzHKEJ6ga2LzIL7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/methods": "^1.1.4", + "@types/superagent": "^8.1.0" + } + }, + "node_modules/@types/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/validator": { + "version": "13.15.10", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.10.tgz", + "integrity": "sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==", + "license": "MIT" + }, + "node_modules/@types/yargs": { + "version": "17.0.35", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", + "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.48.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.48.0.tgz", + "integrity": "sha512-XxXP5tL1txl13YFtrECECQYeZjBZad4fyd3cFV4a19LkAY/bIp9fev3US4S5fDVV2JaYFiKAZ/GRTOLer+mbyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.48.0", + "@typescript-eslint/type-utils": "8.48.0", + "@typescript-eslint/utils": "8.48.0", + "@typescript-eslint/visitor-keys": "8.48.0", + "graphemer": "^1.4.0", + "ignore": "^7.0.0", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.48.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.48.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.48.0.tgz", + "integrity": "sha512-jCzKdm/QK0Kg4V4IK/oMlRZlY+QOcdjv89U2NgKHZk1CYTj82/RVSx1mV/0gqCVMJ/DA+Zf/S4NBWNF8GQ+eqQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@typescript-eslint/scope-manager": "8.48.0", + "@typescript-eslint/types": "8.48.0", + "@typescript-eslint/typescript-estree": "8.48.0", + "@typescript-eslint/visitor-keys": "8.48.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.48.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.48.0.tgz", + "integrity": "sha512-Ne4CTZyRh1BecBf84siv42wv5vQvVmgtk8AuiEffKTUo3DrBaGYZueJSxxBZ8fjk/N3DrgChH4TOdIOwOwiqqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.48.0", + "@typescript-eslint/types": "^8.48.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.48.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.48.0.tgz", + "integrity": "sha512-uGSSsbrtJrLduti0Q1Q9+BF1/iFKaxGoQwjWOIVNJv0o6omrdyR8ct37m4xIl5Zzpkp69Kkmvom7QFTtue89YQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.48.0", + "@typescript-eslint/visitor-keys": "8.48.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.48.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.48.0.tgz", + "integrity": "sha512-WNebjBdFdyu10sR1M4OXTt2OkMd5KWIL+LLfeH9KhgP+jzfDV/LI3eXzwJ1s9+Yc0Kzo2fQCdY/OpdusCMmh6w==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.48.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.48.0.tgz", + "integrity": "sha512-zbeVaVqeXhhab6QNEKfK96Xyc7UQuoFWERhEnj3mLVnUWrQnv15cJNseUni7f3g557gm0e46LZ6IJ4NJVOgOpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.48.0", + "@typescript-eslint/typescript-estree": "8.48.0", + "@typescript-eslint/utils": "8.48.0", + "debug": "^4.3.4", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.48.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.48.0.tgz", + "integrity": "sha512-cQMcGQQH7kwKoVswD1xdOytxQR60MWKM1di26xSUtxehaDs/32Zpqsu5WJlXTtTTqyAVK8R7hvsUnIXRS+bjvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.48.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.48.0.tgz", + "integrity": "sha512-ljHab1CSO4rGrQIAyizUS6UGHHCiAYhbfcIZ1zVJr5nMryxlXMVWS3duFPSKvSUbFPwkXMFk1k0EMIjub4sRRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.48.0", + "@typescript-eslint/tsconfig-utils": "8.48.0", + "@typescript-eslint/types": "8.48.0", + "@typescript-eslint/visitor-keys": "8.48.0", + "debug": "^4.3.4", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.48.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.48.0.tgz", + "integrity": "sha512-yTJO1XuGxCsSfIVt1+1UrLHtue8xz16V8apzPYI06W0HbEbEWHxHXgZaAgavIkoh+GeV6hKKd5jm0sS6OYxWXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.48.0", + "@typescript-eslint/types": "8.48.0", + "@typescript-eslint/typescript-estree": "8.48.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.48.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.48.0.tgz", + "integrity": "sha512-T0XJMaRPOH3+LBbAfzR2jalckP1MSG/L9eUtY0DEzUyVaXJ/t6zN0nR7co5kz0Jko/nkSYCBRkz1djvjajVTTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.48.0", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "dev": true, + "license": "ISC" + }, + "node_modules/@unrs/resolver-binding-android-arm-eabi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz", + "integrity": "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-android-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.11.1.tgz", + "integrity": "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.11.1.tgz", + "integrity": "sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.11.1.tgz", + "integrity": "sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-freebsd-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.11.1.tgz", + "integrity": "sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.11.1.tgz", + "integrity": "sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.11.1.tgz", + "integrity": "sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.11.1.tgz", + "integrity": "sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.11.1.tgz", + "integrity": "sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.11.1.tgz", + "integrity": "sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.11.1.tgz", + "integrity": "sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.11.1.tgz", + "integrity": "sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.11.1.tgz", + "integrity": "sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.11.1.tgz", + "integrity": "sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.11.1.tgz", + "integrity": "sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-wasm32-wasi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.11.1.tgz", + "integrity": "sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^0.2.11" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.11.1.tgz", + "integrity": "sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.11.1.tgz", + "integrity": "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-x64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz", + "integrity": "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-import-phases": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", + "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + }, + "peerDependencies": { + "acorn": "^8.14.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-formats/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ansis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/ansis/-/ansis-4.2.0.tgz", + "integrity": "sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", + "license": "MIT" + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/array-timsort": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array-timsort/-/array-timsort-1.0.3.tgz", + "integrity": "sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/aws-ssl-profiles": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz", + "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/babel-jest": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.2.0.tgz", + "integrity": "sha512-0YiBEOxWqKkSQWL9nNGGEgndoeL0ZpWrbLMNL5u/Kaxrli3Eaxlt3ZtIDktEvXt4L/R9r3ODr2zKwGM/2BjxVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/transform": "30.2.0", + "@types/babel__core": "^7.20.5", + "babel-plugin-istanbul": "^7.0.1", + "babel-preset-jest": "30.2.0", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.11.0 || ^8.0.0-0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-7.0.1.tgz", + "integrity": "sha512-D8Z6Qm8jCvVXtIRkBnqNHX0zJ37rQcFJ9u8WOS6tkYOsRdHBzypCstaxWiu5ZIlqQtviRYbgnRLSoCEvjqcqbA==", + "dev": true, + "license": "BSD-3-Clause", + "workspaces": [ + "test/babel-8" + ], + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-instrument": "^6.0.2", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-30.2.0.tgz", + "integrity": "sha512-ftzhzSGMUnOzcCXd6WHdBGMyuwy15Wnn0iyyWGKgBDLxf9/s5ABuraCSpBX2uG0jUg4rqJnxsLc5+oYBqoxVaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/babel__core": "^7.20.5" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", + "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/babel-preset-jest": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-30.2.0.tgz", + "integrity": "sha512-US4Z3NOieAQumwFnYdUWKvUKh8+YSnS/gB3t6YBiz0bskpu7Pine8pPCheNxlPEW4wnUkma2a94YuW2q3guvCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "30.2.0", + "babel-preset-current-node-syntax": "^1.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.11.0 || ^8.0.0-beta.1" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.8.32", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.32.tgz", + "integrity": "sha512-OPz5aBThlyLFgxyhdwf/s2+8ab3OvT7AdTNvKHBwpXomIYeXqpUUuT8LrdtxZSsWJ4R4CU1un4XGh5Ez3nlTpw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/body-parser": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.1.tgz", + "integrity": "sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.0", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.0.tgz", + "integrity": "sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "baseline-browser-mapping": "^2.8.25", + "caniuse-lite": "^1.0.30001754", + "electron-to-chromium": "^1.5.249", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.1.4" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-json-stable-stringify": "2.x" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/c12": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/c12/-/c12-3.1.0.tgz", + "integrity": "sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "chokidar": "^4.0.3", + "confbox": "^0.2.2", + "defu": "^6.1.4", + "dotenv": "^16.6.1", + "exsolve": "^1.0.7", + "giget": "^2.0.0", + "jiti": "^2.4.2", + "ohash": "^2.0.11", + "pathe": "^2.0.3", + "perfect-debounce": "^1.0.0", + "pkg-types": "^2.2.0", + "rc9": "^2.1.2" + }, + "peerDependencies": { + "magicast": "^0.3.5" + }, + "peerDependenciesMeta": { + "magicast": { + "optional": true + } + } + }, + "node_modules/c12/node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "devOptional": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001757", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001757.tgz", + "integrity": "sha512-r0nnL/I28Zi/yjk1el6ilj27tKcdjLsNqAOZr0yVjWPrSQyHgKI2INaEWw21bAQSv2LXRt1XuCS/GomNpWOxsQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/chardet": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.1.tgz", + "integrity": "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/chevrotain": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-10.5.0.tgz", + "integrity": "sha512-Pkv5rBY3+CsHOYfV5g/Vs5JY9WTHHDEKOlohI2XeygaZhUeqhAlldZ8Hz9cRmxu709bvS08YzxHdTPHhffc13A==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/cst-dts-gen": "10.5.0", + "@chevrotain/gast": "10.5.0", + "@chevrotain/types": "10.5.0", + "@chevrotain/utils": "10.5.0", + "lodash": "4.17.21", + "regexp-to-ast": "0.5.0" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "devOptional": true, + "license": "MIT", + "peer": true, + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/chrome-trace-event": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0" + } + }, + "node_modules/ci-info": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.1.tgz", + "integrity": "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/citty": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz", + "integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "consola": "^3.2.3" + } + }, + "node_modules/cjs-module-lexer": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.1.1.tgz", + "integrity": "sha512-+CmxIZ/L2vNcEfvNtLdU0ZQ6mbq3FZnwAP2PPTiKP+1QOoKwlKlPgb8UKV0Dds7QVaMnHm+FwSft2VB0s/SLjQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/class-transformer": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz", + "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==", + "license": "MIT", + "peer": true + }, + "node_modules/class-validator": { + "version": "0.14.3", + "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.3.tgz", + "integrity": "sha512-rXXekcjofVN1LTOSw+u4u9WXVEUvNBVjORW154q/IdmYWy1nMbOU9aNtZB0t8m+FJQ9q91jlr2f9CwwUFdFMRA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/validator": "^13.15.3", + "libphonenumber-js": "^1.11.1", + "validator": "^13.15.20" + } + }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-table3": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.5.tgz", + "integrity": "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "string-width": "^4.2.0" + }, + "engines": { + "node": "10.* || >= 12.*" + }, + "optionalDependencies": { + "@colors/colors": "1.5.0" + } + }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz", + "integrity": "sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==", + "dev": true, + "license": "MIT" + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/comment-json": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/comment-json/-/comment-json-4.4.1.tgz", + "integrity": "sha512-r1To31BQD5060QdkC+Iheai7gHwoSZobzunqkf2/kQ6xIAfJyrKNAFUwdKvkK7Qgu7pVTKQEa7ok7Ed3ycAJgg==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-timsort": "^1.0.3", + "core-util-is": "^1.0.3", + "esprima": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/component-emitter": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", + "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "engines": [ + "node >= 6.0" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/confbox": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.2.tgz", + "integrity": "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/content-disposition": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", + "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cookiejar": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", + "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", + "dev": true, + "license": "MIT" + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/cosmiconfig": { + "version": "8.3.6", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", + "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0", + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/dedent": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.0.tgz", + "integrity": "sha512-HGFtf8yhuhGhqO07SV79tRp+br4MnbdjeVxotpn1QBl30pcLLCQjX5b2295ll0fv8RKDKsmWYrl05usHM9CewQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/deepmerge-ts": { + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz", + "integrity": "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==", + "devOptional": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/defu": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", + "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "devOptional": true, + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destr": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", + "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/dezalgo": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", + "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", + "dev": true, + "license": "ISC", + "dependencies": { + "asap": "^2.0.0", + "wrappy": "1" + } + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dotenv": { + "version": "17.2.3", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz", + "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dotenv-cli": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/dotenv-cli/-/dotenv-cli-11.0.0.tgz", + "integrity": "sha512-r5pA8idbk7GFWuHEU7trSTflWcdBpQEK+Aw17UrSHjS6CReuhrrPcyC3zcQBPQvhArRHnBo/h6eLH1fkCvNlww==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.6", + "dotenv": "^17.1.0", + "dotenv-expand": "^12.0.0", + "minimist": "^1.2.6" + }, + "bin": { + "dotenv": "cli.js" + } + }, + "node_modules/dotenv-expand": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-12.0.1.tgz", + "integrity": "sha512-LaKRbou8gt0RNID/9RoI+J2rvXsBRPMV7p+ElHlPhcSARbCPDYcYG2s1TIzAfWv4YSgyY5taidWzzs31lNV3yQ==", + "license": "BSD-2-Clause", + "dependencies": { + "dotenv": "^16.4.5" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dotenv-expand/node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/effect": { + "version": "3.18.4", + "resolved": "https://registry.npmjs.org/effect/-/effect-3.18.4.tgz", + "integrity": "sha512-b1LXQJLe9D11wfnOKAk3PKxuqYshQ0Heez+y5pnkd3jLj1yx9QhM72zZ9uUrOQyNvrs2GZZd/3maL0ZV18YuDA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "fast-check": "^3.23.1" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.262", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.262.tgz", + "integrity": "sha512-NlAsMteRHek05jRUxUR0a5jpjYq9ykk6+kO0yRaMi5moe7u0fVIOeQ3Y30A8dIiWFBNUoQGi1ljb1i5VtS9WQQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/empathic": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.0.tgz", + "integrity": "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.18.3", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", + "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.1.tgz", + "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.39.1", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-config-prettier": { + "version": "10.1.8", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", + "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "funding": { + "url": "https://opencollective.com/eslint-config-prettier" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-prettier": { + "version": "5.5.4", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.4.tgz", + "integrity": "sha512-swNtI95SToIz05YINMA6Ox5R057IMAmWZ26GqPxusAp1TZzj+IdY9tXNWWD3vkF/wEqydCONcwjTFpxybBqZsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "prettier-linter-helpers": "^1.0.0", + "synckit": "^0.11.7" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-plugin-prettier" + }, + "peerDependencies": { + "@types/eslint": ">=8.0.0", + "eslint": ">=8.0.0", + "eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0", + "prettier": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@types/eslint": { + "optional": true + }, + "eslint-config-prettier": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/execa/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/exit-x": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/exit-x/-/exit-x-0.2.2.tgz", + "integrity": "sha512-+I6B/IkJc1o/2tiURyz/ivu/O0nKNEArIUB5O7zBrlDVJr22SCLH3xTeEry428LvFhRzIA1g8izguxJ/gbNcVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-30.2.0.tgz", + "integrity": "sha512-u/feCi0GPsI+988gU2FLcsHyAHTU0MX1Wg68NhAnN7z/+C5wqG+CY8J53N9ioe8RXgaoz0nBR/TYMf3AycUuPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "30.2.0", + "@jest/get-type": "30.1.0", + "jest-matcher-utils": "30.2.0", + "jest-message-util": "30.2.0", + "jest-mock": "30.2.0", + "jest-util": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/express": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", + "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.0", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/exsolve": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz", + "integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/fast-check": { + "version": "3.23.2", + "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.23.2.tgz", + "integrity": "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==", + "devOptional": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT", + "dependencies": { + "pure-rand": "^6.1.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/fast-check/node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "devOptional": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fflate": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", + "license": "MIT" + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/file-type": { + "version": "21.1.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-21.1.0.tgz", + "integrity": "sha512-boU4EHmP3JXkwDo4uhyBhTt5pPstxB6eEXKJBu2yu2l7aAMMm7QQYQEzssJmKReZYrFdFOJS8koVo6bXIBGDqA==", + "license": "MIT", + "dependencies": { + "@tokenizer/inflate": "^0.3.1", + "strtok3": "^10.3.1", + "token-types": "^6.0.0", + "uint8array-extras": "^1.4.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", + "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fork-ts-checker-webpack-plugin": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-9.1.0.tgz", + "integrity": "sha512-mpafl89VFPJmhnJ1ssH+8wmM2b50n+Rew5x42NeI2U78aRWgtkEtGmctp7iT16UjquJTjorEmIfESj3DxdW84Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.16.7", + "chalk": "^4.1.2", + "chokidar": "^4.0.1", + "cosmiconfig": "^8.2.0", + "deepmerge": "^4.2.2", + "fs-extra": "^10.0.0", + "memfs": "^3.4.1", + "minimatch": "^3.0.4", + "node-abort-controller": "^3.0.1", + "schema-utils": "^3.1.1", + "semver": "^7.3.5", + "tapable": "^2.2.1" + }, + "engines": { + "node": ">=14.21.3" + }, + "peerDependencies": { + "typescript": ">3.6.0", + "webpack": "^5.11.0" + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/form-data/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/form-data/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/formidable": { + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.4.tgz", + "integrity": "sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@paralleldrive/cuid2": "^2.2.2", + "dezalgo": "^1.0.4", + "once": "^1.4.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "url": "https://ko-fi.com/tunnckoCore/commissions" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/fs-monkey": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.1.0.tgz", + "integrity": "sha512-QMUezzXWII9EV5aTFXW1UBVUO77wYPpjqIF8/AviUCThNeSYZykpoTixUeaNNBwmCev0AMDWMAni+f8Hxb1IFw==", + "dev": true, + "license": "Unlicense" + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generate-function": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "is-property": "^1.0.2" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-port-please": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/get-port-please/-/get-port-please-3.1.2.tgz", + "integrity": "sha512-Gxc29eLs1fbn6LQ4jSU4vXjlwyZhF5HsGuMAa7gqBP4Rw4yxxltyDUuF5MBclFzDTXO+ACchGQoeela4DSfzdQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/giget": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/giget/-/giget-2.0.0.tgz", + "integrity": "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "citty": "^0.1.6", + "consola": "^3.4.0", + "defu": "^6.1.4", + "node-fetch-native": "^1.6.6", + "nypm": "^0.6.0", + "pathe": "^2.0.3" + }, + "bin": { + "giget": "dist/cli.mjs" + } + }, + "node_modules/glob": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.0.tgz", + "integrity": "sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.1.1", + "minipass": "^7.1.2", + "path-scurry": "^2.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/glob/node_modules/minimatch": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz", + "integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/brace-expansion": "^5.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/globals": { + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz", + "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "devOptional": true, + "license": "ISC" + }, + "node_modules/grammex": { + "version": "3.1.12", + "resolved": "https://registry.npmjs.org/grammex/-/grammex-3.1.12.tgz", + "integrity": "sha512-6ufJOsSA7LcQehIJNCO7HIBykfM7DXQual0Ny780/DEcJIpBlHRvcqEBWGPYd7hrXL2GJ3oJI1MIhaXjWmLQOQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/handlebars": { + "version": "4.7.8", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", + "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, + "node_modules/handlebars/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hono": { + "version": "4.7.10", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.7.10.tgz", + "integrity": "sha512-QkACju9MiN59CKSY5JsGZCYmPZkA6sIW6OFCUp7qDjZu6S6KHtJHhAc9Uy9mV9F8PJ1/HQ3ybZF2yjCa/73fvQ==", + "devOptional": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/http-status-codes": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/http-status-codes/-/http-status-codes-2.3.0.tgz", + "integrity": "sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", + "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "devOptional": true, + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/iterare": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/iterare/-/iterare-1.2.1.tgz", + "integrity": "sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q==", + "license": "ISC", + "engines": { + "node": ">=6" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jest": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-30.2.0.tgz", + "integrity": "sha512-F26gjC0yWN8uAA5m5Ss8ZQf5nDHWGlN/xWZIh8S5SRbsEKBovwZhxGd6LJlbZYxBgCYOtreSUyb8hpXyGC5O4A==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@jest/core": "30.2.0", + "@jest/types": "30.2.0", + "import-local": "^3.2.0", + "jest-cli": "30.2.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-30.2.0.tgz", + "integrity": "sha512-L8lR1ChrRnSdfeOvTrwZMlnWV8G/LLjQ0nG9MBclwWZidA2N5FviRki0Bvh20WRMOX31/JYvzdqTJrk5oBdydQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^5.1.1", + "jest-util": "30.2.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-circus": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-30.2.0.tgz", + "integrity": "sha512-Fh0096NC3ZkFx05EP2OXCxJAREVxj1BcW/i6EWqqymcgYKWjyyDpral3fMxVcHXg6oZM7iULer9wGRFvfpl+Tg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.2.0", + "@jest/expect": "30.2.0", + "@jest/test-result": "30.2.0", + "@jest/types": "30.2.0", + "@types/node": "*", + "chalk": "^4.1.2", + "co": "^4.6.0", + "dedent": "^1.6.0", + "is-generator-fn": "^2.1.0", + "jest-each": "30.2.0", + "jest-matcher-utils": "30.2.0", + "jest-message-util": "30.2.0", + "jest-runtime": "30.2.0", + "jest-snapshot": "30.2.0", + "jest-util": "30.2.0", + "p-limit": "^3.1.0", + "pretty-format": "30.2.0", + "pure-rand": "^7.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-cli": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-30.2.0.tgz", + "integrity": "sha512-Os9ukIvADX/A9sLt6Zse3+nmHtHaE6hqOsjQtNiugFTbKRHYIYtZXNGNK9NChseXy7djFPjndX1tL0sCTlfpAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "30.2.0", + "@jest/test-result": "30.2.0", + "@jest/types": "30.2.0", + "chalk": "^4.1.2", + "exit-x": "^0.2.2", + "import-local": "^3.2.0", + "jest-config": "30.2.0", + "jest-util": "30.2.0", + "jest-validate": "30.2.0", + "yargs": "^17.7.2" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-30.2.0.tgz", + "integrity": "sha512-g4WkyzFQVWHtu6uqGmQR4CQxz/CH3yDSlhzXMWzNjDx843gYjReZnMRanjRCq5XZFuQrGDxgUaiYWE8BRfVckA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", + "@jest/get-type": "30.1.0", + "@jest/pattern": "30.0.1", + "@jest/test-sequencer": "30.2.0", + "@jest/types": "30.2.0", + "babel-jest": "30.2.0", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "deepmerge": "^4.3.1", + "glob": "^10.3.10", + "graceful-fs": "^4.2.11", + "jest-circus": "30.2.0", + "jest-docblock": "30.2.0", + "jest-environment-node": "30.2.0", + "jest-regex-util": "30.0.1", + "jest-resolve": "30.2.0", + "jest-runner": "30.2.0", + "jest-util": "30.2.0", + "jest-validate": "30.2.0", + "micromatch": "^4.0.8", + "parse-json": "^5.2.0", + "pretty-format": "30.2.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "esbuild-register": ">=3.4.0", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "esbuild-register": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-config/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/jest-config/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jest-config/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/jest-config/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jest-config/node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jest-diff": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.2.0.tgz", + "integrity": "sha512-dQHFo3Pt4/NLlG5z4PxZ/3yZTZ1C7s9hveiOj+GCN+uT109NC2QgsoVZsVOAvbJ3RgKkvyLGXZV9+piDpWbm6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/diff-sequences": "30.0.1", + "@jest/get-type": "30.1.0", + "chalk": "^4.1.2", + "pretty-format": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-30.2.0.tgz", + "integrity": "sha512-tR/FFgZKS1CXluOQzZvNH3+0z9jXr3ldGSD8bhyuxvlVUwbeLOGynkunvlTMxchC5urrKndYiwCFC0DLVjpOCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-newline": "^3.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-each": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-30.2.0.tgz", + "integrity": "sha512-lpWlJlM7bCUf1mfmuqTA8+j2lNURW9eNafOy99knBM01i5CQeY5UH1vZjgT9071nDJac1M4XsbyI44oNOdhlDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "@jest/types": "30.2.0", + "chalk": "^4.1.2", + "jest-util": "30.2.0", + "pretty-format": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-30.2.0.tgz", + "integrity": "sha512-ElU8v92QJ9UrYsKrxDIKCxu6PfNj4Hdcktcn0JX12zqNdqWHB0N+hwOnnBBXvjLd2vApZtuLUGs1QSY+MsXoNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.2.0", + "@jest/fake-timers": "30.2.0", + "@jest/types": "30.2.0", + "@types/node": "*", + "jest-mock": "30.2.0", + "jest-util": "30.2.0", + "jest-validate": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.2.0.tgz", + "integrity": "sha512-sQA/jCb9kNt+neM0anSj6eZhLZUIhQgwDt7cPGjumgLM4rXsfb9kpnlacmvZz3Q5tb80nS+oG/if+NBKrHC+Xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.2.0", + "@types/node": "*", + "anymatch": "^3.1.3", + "fb-watchman": "^2.0.2", + "graceful-fs": "^4.2.11", + "jest-regex-util": "30.0.1", + "jest-util": "30.2.0", + "jest-worker": "30.2.0", + "micromatch": "^4.0.8", + "walker": "^1.0.8" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.3" + } + }, + "node_modules/jest-leak-detector": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-30.2.0.tgz", + "integrity": "sha512-M6jKAjyzjHG0SrQgwhgZGy9hFazcudwCNovY/9HPIicmNSBuockPSedAP9vlPK6ONFJ1zfyH/M2/YYJxOz5cdQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "pretty-format": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.2.0.tgz", + "integrity": "sha512-dQ94Nq4dbzmUWkQ0ANAWS9tBRfqCrn0bV9AMYdOi/MHW726xn7eQmMeRTpX2ViC00bpNaWXq+7o4lIQ3AX13Hg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "chalk": "^4.1.2", + "jest-diff": "30.2.0", + "pretty-format": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.2.0.tgz", + "integrity": "sha512-y4DKFLZ2y6DxTWD4cDe07RglV88ZiNEdlRfGtqahfbIjfsw1nMCPx49Uev4IA/hWn3sDKyAnSPwoYSsAEdcimw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@jest/types": "30.2.0", + "@types/stack-utils": "^2.0.3", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "micromatch": "^4.0.8", + "pretty-format": "30.2.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-mock": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.2.0.tgz", + "integrity": "sha512-JNNNl2rj4b5ICpmAcq+WbLH83XswjPbjH4T7yvGzfAGCPh1rw+xVNbtk+FnRslvt9lkCcdn9i1oAoKUuFsOxRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.2.0", + "@types/node": "*", + "jest-util": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz", + "integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-30.2.0.tgz", + "integrity": "sha512-TCrHSxPlx3tBY3hWNtRQKbtgLhsXa1WmbJEqBlTBrGafd5fiQFByy2GNCEoGR+Tns8d15GaL9cxEzKOO3GEb2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.2.0", + "jest-pnp-resolver": "^1.2.3", + "jest-util": "30.2.0", + "jest-validate": "30.2.0", + "slash": "^3.0.0", + "unrs-resolver": "^1.7.11" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-30.2.0.tgz", + "integrity": "sha512-xTOIGug/0RmIe3mmCqCT95yO0vj6JURrn1TKWlNbhiAefJRWINNPgwVkrVgt/YaerPzY3iItufd80v3lOrFJ2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-regex-util": "30.0.1", + "jest-snapshot": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-runner": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-30.2.0.tgz", + "integrity": "sha512-PqvZ2B2XEyPEbclp+gV6KO/F1FIFSbIwewRgmROCMBo/aZ6J1w8Qypoj2pEOcg3G2HzLlaP6VUtvwCI8dM3oqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "30.2.0", + "@jest/environment": "30.2.0", + "@jest/test-result": "30.2.0", + "@jest/transform": "30.2.0", + "@jest/types": "30.2.0", + "@types/node": "*", + "chalk": "^4.1.2", + "emittery": "^0.13.1", + "exit-x": "^0.2.2", + "graceful-fs": "^4.2.11", + "jest-docblock": "30.2.0", + "jest-environment-node": "30.2.0", + "jest-haste-map": "30.2.0", + "jest-leak-detector": "30.2.0", + "jest-message-util": "30.2.0", + "jest-resolve": "30.2.0", + "jest-runtime": "30.2.0", + "jest-util": "30.2.0", + "jest-watcher": "30.2.0", + "jest-worker": "30.2.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-runner/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/jest-runner/node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/jest-runtime": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-30.2.0.tgz", + "integrity": "sha512-p1+GVX/PJqTucvsmERPMgCPvQJpFt4hFbM+VN3n8TMo47decMUcJbt+rgzwrEme0MQUA/R+1de2axftTHkKckg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.2.0", + "@jest/fake-timers": "30.2.0", + "@jest/globals": "30.2.0", + "@jest/source-map": "30.0.1", + "@jest/test-result": "30.2.0", + "@jest/transform": "30.2.0", + "@jest/types": "30.2.0", + "@types/node": "*", + "chalk": "^4.1.2", + "cjs-module-lexer": "^2.1.0", + "collect-v8-coverage": "^1.0.2", + "glob": "^10.3.10", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.2.0", + "jest-message-util": "30.2.0", + "jest-mock": "30.2.0", + "jest-regex-util": "30.0.1", + "jest-resolve": "30.2.0", + "jest-snapshot": "30.2.0", + "jest-util": "30.2.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-runtime/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/jest-runtime/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jest-runtime/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/jest-runtime/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jest-runtime/node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jest-snapshot": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-30.2.0.tgz", + "integrity": "sha512-5WEtTy2jXPFypadKNpbNkZ72puZCa6UjSr/7djeecHWOu7iYhSXSnHScT8wBz3Rn8Ena5d5RYRcsyKIeqG1IyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", + "@babel/generator": "^7.27.5", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/plugin-syntax-typescript": "^7.27.1", + "@babel/types": "^7.27.3", + "@jest/expect-utils": "30.2.0", + "@jest/get-type": "30.1.0", + "@jest/snapshot-utils": "30.2.0", + "@jest/transform": "30.2.0", + "@jest/types": "30.2.0", + "babel-preset-current-node-syntax": "^1.2.0", + "chalk": "^4.1.2", + "expect": "30.2.0", + "graceful-fs": "^4.2.11", + "jest-diff": "30.2.0", + "jest-matcher-utils": "30.2.0", + "jest-message-util": "30.2.0", + "jest-util": "30.2.0", + "pretty-format": "30.2.0", + "semver": "^7.7.2", + "synckit": "^0.11.8" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-util": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.2.0.tgz", + "integrity": "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.2.0", + "@types/node": "*", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-validate": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-30.2.0.tgz", + "integrity": "sha512-FBGWi7dP2hpdi8nBoWxSsLvBFewKAg0+uSQwBaof4Y4DPgBabXgpSYC5/lR7VmnIlSpASmCi/ntRWPbv7089Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "@jest/types": "30.2.0", + "camelcase": "^6.3.0", + "chalk": "^4.1.2", + "leven": "^3.1.0", + "pretty-format": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watcher": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-30.2.0.tgz", + "integrity": "sha512-PYxa28dxJ9g777pGm/7PrbnMeA0Jr7osHP9bS7eJy9DuAjMgdGtxgf0uKMyoIsTWAkIbUW5hSDdJ3urmgXBqxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "30.2.0", + "@jest/types": "30.2.0", + "@types/node": "*", + "ansi-escapes": "^4.3.2", + "chalk": "^4.1.2", + "emittery": "^0.13.1", + "jest-util": "30.2.0", + "string-length": "^4.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-worker": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.2.0.tgz", + "integrity": "sha512-0Q4Uk8WF7BUwqXHuAjc23vmopWJw5WH7w2tqBoUOZpOjW/ZnR44GXXd1r82RvnmI2GZge3ivrYXk/BE2+VtW2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@ungap/structured-clone": "^1.3.0", + "jest-util": "30.2.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.1.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "devOptional": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "license": "MIT", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jwa": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz", + "integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "license": "MIT", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/libphonenumber-js": { + "version": "1.12.29", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.12.29.tgz", + "integrity": "sha512-P2aLrbeqHbmh8+9P35LXQfXOKc7XJ0ymUKl7tyeyQjdRNfzunXWxQXGc4yl3fUf28fqLRfPY+vIVvFXK7KEBTw==", + "license": "MIT" + }, + "node_modules/lilconfig": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", + "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/load-esm": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/load-esm/-/load-esm-1.0.3.tgz", + "integrity": "sha512-v5xlu8eHD1+6r8EHTg6hfmO97LN8ugKtiXcy5e6oN72iD2r6u0RPfLl6fxM+7Wnh2ZRq15o0russMst44WauPA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + }, + { + "type": "buymeacoffee", + "url": "https://buymeacoffee.com/borewit" + } + ], + "license": "MIT", + "engines": { + "node": ">=13.2.0" + } + }, + "node_modules/loader-runner": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz", + "integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.11.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lru.min": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.3.tgz", + "integrity": "sha512-Lkk/vx6ak3rYkRR0Nhu4lFUT2VDnQSxBe8Hbl7f36358p6ow8Bnvr8lrLt98H8J1aGxfhbX4Fs5tYg2+FTwr5Q==", + "devOptional": true, + "license": "MIT", + "engines": { + "bun": ">=1.0.0", + "deno": ">=1.30.0", + "node": ">=8.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wellwelwel" + } + }, + "node_modules/magic-string": { + "version": "0.30.17", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", + "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/memfs": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.5.3.tgz", + "integrity": "sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==", + "dev": true, + "license": "Unlicense", + "dependencies": { + "fs-monkey": "^1.0.4" + }, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/multer": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/multer/-/multer-2.0.2.tgz", + "integrity": "sha512-u7f2xaZ/UG8oLXHvtF/oWTRvT44p9ecwBBqTwgJVq0+4BW1g8OW01TyMEGWBHbyMOYVHXslaut7qEQ1meATXgw==", + "license": "MIT", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.6.0", + "concat-stream": "^2.0.0", + "mkdirp": "^0.5.6", + "object-assign": "^4.1.1", + "type-is": "^1.6.18", + "xtend": "^4.0.2" + }, + "engines": { + "node": ">= 10.16.0" + } + }, + "node_modules/multer/node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/multer/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/multer/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/multer/node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mute-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", + "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/mysql2": { + "version": "3.15.3", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.15.3.tgz", + "integrity": "sha512-FBrGau0IXmuqg4haEZRBfHNWB5mUARw6hNwPDXXGg0XzVJ50mr/9hb267lvpVMnhZ1FON3qNd4Xfcez1rbFwSg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "aws-ssl-profiles": "^1.1.1", + "denque": "^2.1.0", + "generate-function": "^2.3.1", + "iconv-lite": "^0.7.0", + "long": "^5.2.1", + "lru.min": "^1.0.0", + "named-placeholders": "^1.1.3", + "seq-queue": "^0.0.5", + "sqlstring": "^2.3.2" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/named-placeholders": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.3.tgz", + "integrity": "sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "lru-cache": "^7.14.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/named-placeholders/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "devOptional": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/napi-postinstall": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz", + "integrity": "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==", + "dev": true, + "license": "MIT", + "bin": { + "napi-postinstall": "lib/cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/napi-postinstall" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-abort-controller": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz", + "integrity": "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-emoji": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.11.0.tgz", + "integrity": "sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash": "^4.17.21" + } + }, + "node_modules/node-fetch-native": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz", + "integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nypm": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.2.tgz", + "integrity": "sha512-7eM+hpOtrKrBDCh7Ypu2lJ9Z7PNZBdi/8AT3AX8xoCj43BBVHD0hPSTEvMtkMpfs8FCqBGhxB+uToIQimA111g==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "citty": "^0.1.6", + "consola": "^3.4.2", + "pathe": "^2.0.3", + "pkg-types": "^2.3.0", + "tinyexec": "^1.0.1" + }, + "bin": { + "nypm": "dist/cli.mjs" + }, + "engines": { + "node": "^14.16.0 || >=16.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ohash": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", + "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.1.tgz", + "integrity": "sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.2.tgz", + "integrity": "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/path-to-regexp": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/perfect-debounce": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", + "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/pg": { + "version": "8.16.3", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz", + "integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==", + "license": "MIT", + "peer": true, + "dependencies": { + "pg-connection-string": "^2.9.1", + "pg-pool": "^3.10.1", + "pg-protocol": "^1.10.3", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.2.7" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.2.7.tgz", + "integrity": "sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg==", + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.9.1.tgz", + "integrity": "sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w==", + "license": "MIT" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.10.1.tgz", + "integrity": "sha512-Tu8jMlcX+9d8+QVzKIvM/uJtp07PKr82IUOYEphaWcoBhIYkoHpLXN3qO59nAI11ripznDsEzEv8nUxBVWajGg==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.10.3.tgz", + "integrity": "sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ==", + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "license": "MIT", + "dependencies": { + "split2": "^4.1.0" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-types": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz", + "integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.2.2", + "exsolve": "^1.0.7", + "pathe": "^2.0.3" + } + }, + "node_modules/pluralize": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", + "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres": { + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/postgres/-/postgres-3.4.7.tgz", + "integrity": "sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw==", + "devOptional": true, + "license": "Unlicense", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/porsager" + } + }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", + "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.7.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.7.3.tgz", + "integrity": "sha512-QgODejq9K3OzoBbuyobZlUhznP5SKwPqp+6Q6xw6o8gnhr4O85L2U915iM2IDcfF2NPXVaM9zlo9tdwipnYwzg==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/pretty-format": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz", + "integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/prisma": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-7.0.1.tgz", + "integrity": "sha512-zp93MdFMSU1IHPEXbUHVUuD8wauh2BUm14OVxhxGrWJQQpXpda0rW4VSST2bci4raoldX64/wQxHKkl/wqDskQ==", + "devOptional": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@prisma/config": "7.0.1", + "@prisma/dev": "0.13.0", + "@prisma/engines": "7.0.1", + "@prisma/studio-core": "0.8.2", + "mysql2": "3.15.3", + "postgres": "3.4.7" + }, + "bin": { + "prisma": "build/index.js" + }, + "engines": { + "node": "^20.19 || ^22.12 || >=24.0" + }, + "peerDependencies": { + "better-sqlite3": ">=9.0.0", + "typescript": ">=5.4.0" + }, + "peerDependenciesMeta": { + "better-sqlite3": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/proper-lockfile": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", + "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "retry": "^0.12.0", + "signal-exit": "^3.0.2" + } + }, + "node_modules/proper-lockfile/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "devOptional": true, + "license": "ISC" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/pure-rand": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-7.0.1.tgz", + "integrity": "sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/rc9": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/rc9/-/rc9-2.1.2.tgz", + "integrity": "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "defu": "^6.1.4", + "destr": "^2.0.3" + } + }, + "node_modules/react": { + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", + "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", + "devOptional": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", + "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", + "devOptional": true, + "license": "MIT", + "peer": true, + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.0" + } + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/reflect-metadata": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", + "license": "Apache-2.0", + "peer": true + }, + "node_modules/regexp-to-ast": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/regexp-to-ast/-/regexp-to-ast-0.5.0.tgz", + "integrity": "sha512-tlbJqcMHnPKI9zSrystikWKwHkBqu2a/Sgw01h3zFjvYrMxEDYHzzoMZnUrbIfpTFEsoRnnviOXNCzFiSc54Qw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/remeda": { + "version": "2.21.3", + "resolved": "https://registry.npmjs.org/remeda/-/remeda-2.21.3.tgz", + "integrity": "sha512-XXrZdLA10oEOQhLLzEJEiFFSKi21REGAkHdImIb4rt/XXy8ORGXh5HCcpUOsElfPNDb+X6TA/+wkh+p2KffYmg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "type-fest": "^4.39.1" + } + }, + "node_modules/remeda/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "devOptional": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-cwd/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/restore-cursor/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", + "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.5", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "mime-types": "^3.0.1", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/seq-queue": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz", + "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==", + "devOptional": true + }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "devOptional": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 8" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/sqlstring": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz", + "integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/std-env": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.9.0.tgz", + "integrity": "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strtok3": { + "version": "10.3.4", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.3.4.tgz", + "integrity": "sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg==", + "license": "MIT", + "dependencies": { + "@tokenizer/token": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/superagent": { + "version": "10.2.3", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-10.2.3.tgz", + "integrity": "sha512-y/hkYGeXAj7wUMjxRbB21g/l6aAEituGXM9Rwl4o20+SX3e8YOSV6BxFXl+dL3Uk0mjSL3kCbNkwURm8/gEDig==", + "dev": true, + "license": "MIT", + "dependencies": { + "component-emitter": "^1.3.1", + "cookiejar": "^2.1.4", + "debug": "^4.3.7", + "fast-safe-stringify": "^2.1.1", + "form-data": "^4.0.4", + "formidable": "^3.5.4", + "methods": "^1.1.2", + "mime": "2.6.0", + "qs": "^6.11.2" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/supertest": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/supertest/-/supertest-7.1.4.tgz", + "integrity": "sha512-tjLPs7dVyqgItVFirHYqe2T+MfWc2VOBQ8QFKKbWTA3PU7liZR8zoSpAi/C1k1ilm9RsXIKYf197oap9wXGVYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "methods": "^1.1.2", + "superagent": "^10.2.3" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/symbol-observable": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz", + "integrity": "sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/synckit": { + "version": "0.11.11", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.11.tgz", + "integrity": "sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pkgr/core": "^0.2.9" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/synckit" + } + }, + "node_modules/tapable": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/terser": { + "version": "5.44.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.44.1.tgz", + "integrity": "sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.3.14", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.14.tgz", + "integrity": "sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.0", + "serialize-javascript": "^6.0.2", + "terser": "^5.31.1" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/terser-webpack-plugin/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/terser-webpack-plugin/node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/terser-webpack-plugin/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/terser-webpack-plugin/node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/terser-webpack-plugin/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/terser-webpack-plugin/node_modules/schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/terser-webpack-plugin/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/tinyexec": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", + "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/token-types": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.1.1.tgz", + "integrity": "sha512-kh9LVIWH5CnL63Ipf0jhlBIy0UsrMj/NJDfpsy1SqOXlLKEVyXXYrnFxFT1yOOYVGBSApeVnjPw/sBz5BfEjAQ==", + "license": "MIT", + "dependencies": { + "@borewit/text-codec": "^0.1.0", + "@tokenizer/token": "^0.3.0", + "ieee754": "^1.2.1" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/ts-api-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/ts-jest": { + "version": "29.4.5", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.5.tgz", + "integrity": "sha512-HO3GyiWn2qvTQA4kTgjDcXiMwYQt68a1Y8+JuLRVpdIzm+UOLSHgl/XqR4c6nzJkq5rOkjc02O2I7P7l/Yof0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "bs-logger": "^0.2.6", + "fast-json-stable-stringify": "^2.1.0", + "handlebars": "^4.7.8", + "json5": "^2.2.3", + "lodash.memoize": "^4.1.2", + "make-error": "^1.3.6", + "semver": "^7.7.3", + "type-fest": "^4.41.0", + "yargs-parser": "^21.1.1" + }, + "bin": { + "ts-jest": "cli.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.0 <8", + "@jest/transform": "^29.0.0 || ^30.0.0", + "@jest/types": "^29.0.0 || ^30.0.0", + "babel-jest": "^29.0.0 || ^30.0.0", + "jest": "^29.0.0 || ^30.0.0", + "jest-util": "^29.0.0 || ^30.0.0", + "typescript": ">=4.3 <6" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@jest/transform": { + "optional": true + }, + "@jest/types": { + "optional": true + }, + "babel-jest": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jest-util": { + "optional": true + } + } + }, + "node_modules/ts-jest/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ts-loader": { + "version": "9.5.4", + "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.5.4.tgz", + "integrity": "sha512-nCz0rEwunlTZiy6rXFByQU1kVVpCIgUpc/psFiKVrUwrizdnIbRFu8w7bxhUF0X613DYwT4XzrZHpVyMe758hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "enhanced-resolve": "^5.0.0", + "micromatch": "^4.0.0", + "semver": "^7.3.4", + "source-map": "^0.7.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "typescript": "*", + "webpack": "^5.0.0" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/tsconfig-paths": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", + "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "json5": "^2.2.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tsconfig-paths-webpack-plugin": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths-webpack-plugin/-/tsconfig-paths-webpack-plugin-4.2.0.tgz", + "integrity": "sha512-zbem3rfRS8BgeNK50Zz5SIQgXzLafiHjOwUAvk/38/o1jHn/V5QAgVUcz884or7WYcPaH3N2CIfUc2u0ul7UcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "enhanced-resolve": "^5.7.0", + "tapable": "^2.2.1", + "tsconfig-paths": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/tsconfig-paths/node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "license": "MIT" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "devOptional": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.48.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.48.0.tgz", + "integrity": "sha512-fcKOvQD9GUn3Xw63EgiDqhvWJ5jsyZUaekl3KVpGsDJnN46WJTe3jWxtQP9lMZm1LJNkFLlTaWAxK2vUQR+cqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.48.0", + "@typescript-eslint/parser": "8.48.0", + "@typescript-eslint/typescript-estree": "8.48.0", + "@typescript-eslint/utils": "8.48.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/uglify-js": { + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/uid": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/uid/-/uid-2.0.2.tgz", + "integrity": "sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g==", + "license": "MIT", + "dependencies": { + "@lukeed/csprng": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/uint8array-extras": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.5.0.tgz", + "integrity": "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/unrs-resolver": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.11.1.tgz", + "integrity": "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "napi-postinstall": "^0.3.0" + }, + "funding": { + "url": "https://opencollective.com/unrs-resolver" + }, + "optionalDependencies": { + "@unrs/resolver-binding-android-arm-eabi": "1.11.1", + "@unrs/resolver-binding-android-arm64": "1.11.1", + "@unrs/resolver-binding-darwin-arm64": "1.11.1", + "@unrs/resolver-binding-darwin-x64": "1.11.1", + "@unrs/resolver-binding-freebsd-x64": "1.11.1", + "@unrs/resolver-binding-linux-arm-gnueabihf": "1.11.1", + "@unrs/resolver-binding-linux-arm-musleabihf": "1.11.1", + "@unrs/resolver-binding-linux-arm64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-arm64-musl": "1.11.1", + "@unrs/resolver-binding-linux-ppc64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-riscv64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-riscv64-musl": "1.11.1", + "@unrs/resolver-binding-linux-s390x-gnu": "1.11.1", + "@unrs/resolver-binding-linux-x64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-x64-musl": "1.11.1", + "@unrs/resolver-binding-wasm32-wasi": "1.11.1", + "@unrs/resolver-binding-win32-arm64-msvc": "1.11.1", + "@unrs/resolver-binding-win32-ia32-msvc": "1.11.1", + "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz", + "integrity": "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/uuid": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz", + "integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist-node/bin/uuid" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/valibot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/valibot/-/valibot-1.1.0.tgz", + "integrity": "sha512-Nk8lX30Qhu+9txPYTwM0cFlWLdPFsFr6LblzqIySfbZph9+BFsAHsNvHOymEviUepeIW6KFHzpX8TKhbptBXXw==", + "devOptional": true, + "license": "MIT", + "peerDependencies": { + "typescript": ">=5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/validator": { + "version": "13.15.23", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.23.tgz", + "integrity": "sha512-4yoz1kEWqUjzi5zsPbAS/903QXSYp0UOtHsPpp7p9rHAw/W+dkInskAE386Fat3oKRROwO98d9ZB0G4cObgUyw==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/watchpack": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.4.tgz", + "integrity": "sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "dev": true, + "license": "MIT", + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/webpack": { + "version": "5.103.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.103.0.tgz", + "integrity": "sha512-HU1JOuV1OavsZ+mfigY0j8d1TgQgbZ6M+J75zDkpEAwYeXjWSqrGJtgnPblJjd/mAyTNQ7ygw0MiKOn6etz8yw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.8", + "@types/json-schema": "^7.0.15", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.15.0", + "acorn-import-phases": "^1.0.3", + "browserslist": "^4.26.3", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.17.3", + "es-module-lexer": "^1.2.1", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.3.1", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^4.3.3", + "tapable": "^2.3.0", + "terser-webpack-plugin": "^5.3.11", + "watchpack": "^2.4.4", + "webpack-sources": "^3.3.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-node-externals": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/webpack-node-externals/-/webpack-node-externals-3.0.0.tgz", + "integrity": "sha512-LnL6Z3GGDPht/AigwRh2dvL9PQPFQ8skEpVrWZXLWBYmqcaojHNN0onvHzie6rq7EWKrrBfPYqNEzTJgiwEQDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/webpack-sources": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.3.tgz", + "integrity": "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/webpack/node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/webpack/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/webpack/node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/webpack/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/webpack/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/webpack/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/webpack/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/webpack/node_modules/schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz", + "integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yoctocolors-cjs": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz", + "integrity": "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zeptomatch": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/zeptomatch/-/zeptomatch-2.0.2.tgz", + "integrity": "sha512-H33jtSKf8Ijtb5BW6wua3G5DhnFjbFML36eFu+VdOoVY4HD9e7ggjqdM6639B+L87rjnR6Y+XeRzBXZdy52B/g==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "grammex": "^3.1.10" + } + } + } +} diff --git a/backend/services/backup-service/package.json b/backend/services/backup-service/package.json new file mode 100644 index 00000000..c49d3097 --- /dev/null +++ b/backend/services/backup-service/package.json @@ -0,0 +1,106 @@ +{ + "name": "backup-service", + "version": "1.0.0", + "description": "RWA Durian MPC Backup Share Storage Service", + "author": "RWA Durian Team", + "private": true, + "license": "UNLICENSED", + "scripts": { + "build": "nest build", + "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", + "start": "nest start", + "start:dev": "nest start --watch", + "start:debug": "nest start --debug --watch", + "start:prod": "node dist/main", + "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", + "test": "jest", + "test:watch": "jest --watch", + "test:cov": "jest --coverage", + "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", + "test:e2e": "jest --config ./test/jest-e2e-mock.json", + "test:e2e:mock": "jest --config ./test/jest-e2e-mock.json", + "test:e2e:db": "jest --config ./test/jest-e2e-db.json", + "test:unit": "jest --testPathPatterns=test/unit", + "test:all": "npm run test:unit && npm run test:e2e:mock", + "db:test:up": "docker-compose -f docker-compose.test.yml up -d", + "db:test:down": "docker-compose -f docker-compose.test.yml down -v", + "db:test:setup": "npx ts-node scripts/setup-test-db.ts", + "db:test:migrate": "npx dotenv -e .env.test -- prisma migrate deploy", + "test:e2e:db:manual": "set USE_DOCKER=false && jest --config ./test/jest-e2e-db.json", + "prisma:generate": "prisma generate", + "prisma:migrate": "prisma migrate dev", + "prisma:migrate:prod": "prisma migrate deploy", + "prisma:studio": "prisma studio", + "docker:build": "docker build -t backup-service .", + "docker:up": "docker-compose up -d", + "docker:down": "docker-compose down" + }, + "dependencies": { + "@nestjs/common": "^11.0.1", + "@nestjs/config": "^4.0.2", + "@nestjs/core": "^11.0.1", + "@nestjs/platform-express": "^11.0.1", + "@prisma/adapter-pg": "^7.0.1", + "@prisma/client": "^7.0.1", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.3", + "dotenv": "^17.2.3", + "jsonwebtoken": "^9.0.2", + "reflect-metadata": "^0.2.2", + "rxjs": "^7.8.1", + "uuid": "^13.0.0" + }, + "devDependencies": { + "@eslint/eslintrc": "^3.2.0", + "@eslint/js": "^9.18.0", + "@nestjs/cli": "^11.0.0", + "@nestjs/schematics": "^11.0.0", + "@nestjs/testing": "^11.0.1", + "@types/express": "^5.0.0", + "@types/jest": "^30.0.0", + "@types/jsonwebtoken": "^9.0.10", + "@types/node": "^22.10.7", + "@types/pg": "^8.15.6", + "@types/supertest": "^6.0.2", + "@types/uuid": "^10.0.0", + "dotenv-cli": "^11.0.0", + "eslint": "^9.18.0", + "eslint-config-prettier": "^10.0.1", + "eslint-plugin-prettier": "^5.2.2", + "globals": "^16.0.0", + "jest": "^30.0.0", + "pg": "^8.16.3", + "prettier": "^3.4.2", + "prisma": "^7.0.1", + "source-map-support": "^0.5.21", + "supertest": "^7.0.0", + "ts-jest": "^29.2.5", + "ts-loader": "^9.5.2", + "ts-node": "^10.9.2", + "tsconfig-paths": "^4.2.0", + "typescript": "^5.7.3", + "typescript-eslint": "^8.20.0" + }, + "jest": { + "moduleFileExtensions": [ + "js", + "json", + "ts" + ], + "rootDir": ".", + "testRegex": ".*\\.spec\\.ts$", + "transform": { + "^.+\\.(t|j)s$": "ts-jest" + }, + "collectCoverageFrom": [ + "src/**/*.(t|j)s", + "!src/main.ts", + "!src/**/*.module.ts" + ], + "coverageDirectory": "./coverage", + "testEnvironment": "node", + "moduleNameMapper": { + "^src/(.*)$": "/src/$1" + } + } +} diff --git a/backend/services/backup-service/prisma.config.ts b/backend/services/backup-service/prisma.config.ts new file mode 100644 index 00000000..9c5e9593 --- /dev/null +++ b/backend/services/backup-service/prisma.config.ts @@ -0,0 +1,14 @@ +// This file was generated by Prisma and assumes you have installed the following: +// npm install --save-dev prisma dotenv +import "dotenv/config"; +import { defineConfig, env } from "prisma/config"; + +export default defineConfig({ + schema: "prisma/schema.prisma", + migrations: { + path: "prisma/migrations", + }, + datasource: { + url: env("DATABASE_URL"), + }, +}); diff --git a/backend/services/backup-service/prisma/schema.prisma b/backend/services/backup-service/prisma/schema.prisma new file mode 100644 index 00000000..6e7246b3 --- /dev/null +++ b/backend/services/backup-service/prisma/schema.prisma @@ -0,0 +1,69 @@ +// prisma/schema.prisma + +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgresql" +} + +// 备份分片存储 +model BackupShare { + shareId BigInt @id @default(autoincrement()) @map("share_id") + + // 用户标识 (来自 identity-service) + userId BigInt @unique @map("user_id") + accountSequence BigInt @unique @map("account_sequence") + + // MPC 密钥信息 + publicKey String @unique @map("public_key") @db.VarChar(130) + partyIndex Int @default(2) @map("party_index") // Backup = Party 2 + threshold Int @default(2) + totalParties Int @default(3) @map("total_parties") + + // 加密的分片数据 (AES-256-GCM 加密) + encryptedShareData String @map("encrypted_share_data") @db.Text + encryptionKeyId String @map("encryption_key_id") @db.VarChar(64) // 密钥轮换支持 + + // 状态管理 + status String @default("ACTIVE") @db.VarChar(20) // ACTIVE, REVOKED, ROTATED + + // 访问控制 + accessCount Int @default(0) @map("access_count") // 访问次数限制 + lastAccessedAt DateTime? @map("last_accessed_at") + + // 时间戳 + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + revokedAt DateTime? @map("revoked_at") + + // 索引 + @@index([publicKey], name: "idx_backup_public_key") + @@index([status], name: "idx_backup_status") + @@index([createdAt], name: "idx_backup_created") + @@map("backup_shares") +} + +// 访问审计日志 +model ShareAccessLog { + logId BigInt @id @default(autoincrement()) @map("log_id") + + shareId BigInt @map("share_id") + userId BigInt @map("user_id") + + action String @db.VarChar(20) // STORE, RETRIEVE, REVOKE, ROTATE + sourceService String @map("source_service") @db.VarChar(50) // identity-service, recovery-service + sourceIp String @map("source_ip") @db.VarChar(45) + + success Boolean @default(true) + errorMessage String? @map("error_message") @db.Text + + createdAt DateTime @default(now()) @map("created_at") + + @@index([shareId], name: "idx_log_share") + @@index([userId], name: "idx_log_user") + @@index([action], name: "idx_log_action") + @@index([createdAt], name: "idx_log_created") + @@map("share_access_logs") +} diff --git a/backend/services/backup-service/scripts/setup-test-db.ts b/backend/services/backup-service/scripts/setup-test-db.ts new file mode 100644 index 00000000..bdcb1d0c --- /dev/null +++ b/backend/services/backup-service/scripts/setup-test-db.ts @@ -0,0 +1,89 @@ +/** + * Manual Test Database Setup Script + * + * This script helps set up the test database when Docker is not available. + * It can connect to any PostgreSQL instance and prepare it for E2E testing. + * + * Usage: + * npx ts-node scripts/setup-test-db.ts + * + * Environment variables (or set in .env.test): + * DATABASE_URL - PostgreSQL connection string + */ + +import { execSync } from 'child_process'; +import * as path from 'path'; +import * as dotenv from 'dotenv'; + +// Load test environment +dotenv.config({ path: path.resolve(__dirname, '../.env.test') }); + +async function main() { + console.log('🔧 Manual Test Database Setup'); + console.log('============================\n'); + + const databaseUrl = process.env.DATABASE_URL; + + if (!databaseUrl) { + console.error('❌ DATABASE_URL environment variable is not set'); + console.log('\nPlease set DATABASE_URL in .env.test or as an environment variable'); + console.log('Example: DATABASE_URL="postgresql://postgres:password@localhost:5432/rwa_backup_test"'); + process.exit(1); + } + + console.log(`📌 Database URL: ${databaseUrl.replace(/:[^:@]+@/, ':****@')}\n`); + + // Test database connection + console.log('⏳ Testing database connection...'); + try { + const { Client } = await import('pg'); + const client = new Client({ connectionString: databaseUrl }); + await client.connect(); + const result = await client.query('SELECT version()'); + console.log(`✅ Connected to PostgreSQL: ${result.rows[0].version.split(',')[0]}\n`); + await client.end(); + } catch (error: any) { + console.error(`❌ Failed to connect to database: ${error.message}`); + console.log('\nMake sure:'); + console.log(' 1. PostgreSQL is running'); + console.log(' 2. The database exists'); + console.log(' 3. The credentials are correct'); + process.exit(1); + } + + // Push Prisma schema + console.log('🔄 Pushing Prisma schema to database...'); + try { + execSync('npx prisma db push --force-reset --accept-data-loss', { + cwd: path.resolve(__dirname, '..'), + stdio: 'inherit', + env: { + ...process.env, + DATABASE_URL: databaseUrl, + }, + }); + console.log(''); + } catch (error) { + console.error('❌ Failed to push Prisma schema'); + process.exit(1); + } + + // Generate Prisma client + console.log('🔧 Generating Prisma client...'); + try { + execSync('npx prisma generate', { + cwd: path.resolve(__dirname, '..'), + stdio: 'inherit', + }); + console.log(''); + } catch (error) { + console.error('❌ Failed to generate Prisma client'); + process.exit(1); + } + + console.log('✅ Test database setup complete!\n'); + console.log('You can now run E2E tests with:'); + console.log(' USE_DOCKER=false npm run test:e2e:db\n'); +} + +main().catch(console.error); diff --git a/backend/services/backup-service/src/api/api.module.ts b/backend/services/backup-service/src/api/api.module.ts new file mode 100644 index 00000000..a7092639 --- /dev/null +++ b/backend/services/backup-service/src/api/api.module.ts @@ -0,0 +1,14 @@ +import { Module } from '@nestjs/common'; +import { ConfigModule } from '@nestjs/config'; +import { ApplicationModule } from '../application/application.module'; +import { InfrastructureModule } from '../infrastructure/infrastructure.module'; +import { BackupShareController } from './controllers/backup-share.controller'; +import { HealthController } from './controllers/health.controller'; +import { ServiceAuthGuard } from '../shared/guards/service-auth.guard'; + +@Module({ + imports: [ConfigModule, ApplicationModule, InfrastructureModule], + controllers: [BackupShareController, HealthController], + providers: [ServiceAuthGuard], +}) +export class ApiModule {} diff --git a/backend/services/backup-service/src/api/controllers/backup-share.controller.ts b/backend/services/backup-service/src/api/controllers/backup-share.controller.ts new file mode 100644 index 00000000..f8951ab9 --- /dev/null +++ b/backend/services/backup-service/src/api/controllers/backup-share.controller.ts @@ -0,0 +1,103 @@ +import { + Controller, + Post, + Body, + UseGuards, + HttpCode, + HttpStatus, + Req, +} from '@nestjs/common'; +import { BackupShareApplicationService } from '../../application/services/backup-share-application.service'; +import { StoreBackupShareCommand } from '../../application/commands/store-backup-share/store-backup-share.command'; +import { RevokeShareCommand } from '../../application/commands/revoke-share/revoke-share.command'; +import { GetBackupShareQuery } from '../../application/queries/get-backup-share/get-backup-share.query'; +import { ServiceAuthGuard } from '../../shared/guards/service-auth.guard'; +import { StoreShareDto } from '../dto/request/store-share.dto'; +import { RetrieveShareDto } from '../dto/request/retrieve-share.dto'; +import { RevokeShareDto } from '../dto/request/revoke-share.dto'; +import { + StoreShareResponseDto, + RetrieveShareResponseDto, + RevokeShareResponseDto, +} from '../dto/response/share-info.dto'; + +@Controller('backup-share') +@UseGuards(ServiceAuthGuard) +export class BackupShareController { + constructor( + private readonly backupShareService: BackupShareApplicationService, + ) {} + + @Post('store') + @HttpCode(HttpStatus.CREATED) + async storeShare( + @Body() dto: StoreShareDto, + @Req() request: any, + ): Promise { + const command = new StoreBackupShareCommand( + dto.userId, + dto.accountSequence, + dto.publicKey, + dto.encryptedShareData, + request.sourceService, + request.sourceIp, + dto.threshold, + dto.totalParties, + ); + + const result = await this.backupShareService.storeBackupShare(command); + + return { + success: true, + shareId: result.shareId, + message: 'Backup share stored successfully', + }; + } + + @Post('retrieve') + @HttpCode(HttpStatus.OK) + async retrieveShare( + @Body() dto: RetrieveShareDto, + @Req() request: any, + ): Promise { + const query = new GetBackupShareQuery( + dto.userId, + dto.publicKey, + dto.recoveryToken, + request.sourceService, + request.sourceIp, + dto.deviceId, + ); + + const result = await this.backupShareService.getBackupShare(query); + + return { + success: true, + encryptedShareData: result.encryptedShareData, + partyIndex: result.partyIndex, + publicKey: result.publicKey, + }; + } + + @Post('revoke') + @HttpCode(HttpStatus.OK) + async revokeShare( + @Body() dto: RevokeShareDto, + @Req() request: any, + ): Promise { + const command = new RevokeShareCommand( + dto.userId, + dto.publicKey, + dto.reason, + request.sourceService, + request.sourceIp, + ); + + await this.backupShareService.revokeShare(command); + + return { + success: true, + message: 'Backup share revoked successfully', + }; + } +} diff --git a/backend/services/backup-service/src/api/controllers/health.controller.ts b/backend/services/backup-service/src/api/controllers/health.controller.ts new file mode 100644 index 00000000..67a6f777 --- /dev/null +++ b/backend/services/backup-service/src/api/controllers/health.controller.ts @@ -0,0 +1,44 @@ +import { Controller, Get } from '@nestjs/common'; +import { PrismaService } from '../../infrastructure/persistence/prisma/prisma.service'; + +@Controller('health') +export class HealthController { + constructor(private readonly prisma: PrismaService) {} + + @Get() + async check() { + return { + status: 'ok', + timestamp: new Date().toISOString(), + service: 'backup-service', + }; + } + + @Get('ready') + async readiness() { + try { + // Check database connectivity + await this.prisma.$queryRaw`SELECT 1`; + return { + status: 'ready', + database: 'connected', + timestamp: new Date().toISOString(), + }; + } catch (error) { + return { + status: 'not ready', + database: 'disconnected', + error: error.message, + timestamp: new Date().toISOString(), + }; + } + } + + @Get('live') + async liveness() { + return { + status: 'alive', + timestamp: new Date().toISOString(), + }; + } +} diff --git a/backend/services/backup-service/src/api/dto/request/retrieve-share.dto.ts b/backend/services/backup-service/src/api/dto/request/retrieve-share.dto.ts new file mode 100644 index 00000000..c03701ab --- /dev/null +++ b/backend/services/backup-service/src/api/dto/request/retrieve-share.dto.ts @@ -0,0 +1,25 @@ +import { + IsNotEmpty, + IsString, + IsOptional, + Length, +} from 'class-validator'; + +export class RetrieveShareDto { + @IsNotEmpty() + @IsString() + userId: string; + + @IsNotEmpty() + @IsString() + @Length(66, 130) + publicKey: string; + + @IsNotEmpty() + @IsString() + recoveryToken: string; + + @IsOptional() + @IsString() + deviceId?: string; +} diff --git a/backend/services/backup-service/src/api/dto/request/revoke-share.dto.ts b/backend/services/backup-service/src/api/dto/request/revoke-share.dto.ts new file mode 100644 index 00000000..5bdb986e --- /dev/null +++ b/backend/services/backup-service/src/api/dto/request/revoke-share.dto.ts @@ -0,0 +1,22 @@ +import { + IsNotEmpty, + IsString, + IsIn, + Length, +} from 'class-validator'; + +export class RevokeShareDto { + @IsNotEmpty() + @IsString() + userId: string; + + @IsNotEmpty() + @IsString() + @Length(66, 130) + publicKey: string; + + @IsNotEmpty() + @IsString() + @IsIn(['ROTATION', 'ACCOUNT_CLOSED', 'SECURITY_BREACH', 'USER_REQUEST']) + reason: string; +} diff --git a/backend/services/backup-service/src/api/dto/request/store-share.dto.ts b/backend/services/backup-service/src/api/dto/request/store-share.dto.ts new file mode 100644 index 00000000..8f9696d1 --- /dev/null +++ b/backend/services/backup-service/src/api/dto/request/store-share.dto.ts @@ -0,0 +1,41 @@ +import { + IsNotEmpty, + IsString, + IsNumber, + IsOptional, + Min, + Max, + Length, +} from 'class-validator'; + +export class StoreShareDto { + @IsNotEmpty() + @IsString() + userId: string; + + @IsNotEmpty() + @IsNumber() + @Min(1) + accountSequence: number; + + @IsNotEmpty() + @IsString() + @Length(66, 130) // Compressed or uncompressed public key + publicKey: string; + + @IsNotEmpty() + @IsString() + encryptedShareData: string; + + @IsOptional() + @IsNumber() + @Min(2) + @Max(10) + threshold?: number; + + @IsOptional() + @IsNumber() + @Min(2) + @Max(10) + totalParties?: number; +} diff --git a/backend/services/backup-service/src/api/dto/response/share-info.dto.ts b/backend/services/backup-service/src/api/dto/response/share-info.dto.ts new file mode 100644 index 00000000..24d3fef5 --- /dev/null +++ b/backend/services/backup-service/src/api/dto/response/share-info.dto.ts @@ -0,0 +1,24 @@ +export class StoreShareResponseDto { + success: boolean; + shareId: string; + message: string; +} + +export class RetrieveShareResponseDto { + success: boolean; + encryptedShareData: string; + partyIndex: number; + publicKey: string; +} + +export class RevokeShareResponseDto { + success: boolean; + message: string; +} + +export class ErrorResponseDto { + success: boolean; + error: string; + code: string; + timestamp: string; +} diff --git a/backend/services/backup-service/src/app.module.ts b/backend/services/backup-service/src/app.module.ts new file mode 100644 index 00000000..44804b59 --- /dev/null +++ b/backend/services/backup-service/src/app.module.ts @@ -0,0 +1,35 @@ +import { Module } from '@nestjs/common'; +import { ConfigModule } from '@nestjs/config'; +import { APP_FILTER, APP_INTERCEPTOR } from '@nestjs/core'; +import { ApiModule } from './api/api.module'; +import { ApplicationModule } from './application/application.module'; +import { InfrastructureModule } from './infrastructure/infrastructure.module'; +import { DomainModule } from './domain/domain.module'; +import { GlobalExceptionFilter } from './shared/filters/global-exception.filter'; +import { AuditLogInterceptor } from './shared/interceptors/audit-log.interceptor'; +import configuration from './config'; + +@Module({ + imports: [ + ConfigModule.forRoot({ + isGlobal: true, + load: [configuration], + envFilePath: ['.env', '.env.development', '.env.local'], + }), + DomainModule, + InfrastructureModule, + ApplicationModule, + ApiModule, + ], + providers: [ + { + provide: APP_FILTER, + useClass: GlobalExceptionFilter, + }, + { + provide: APP_INTERCEPTOR, + useClass: AuditLogInterceptor, + }, + ], +}) +export class AppModule {} diff --git a/backend/services/backup-service/src/application/application.module.ts b/backend/services/backup-service/src/application/application.module.ts new file mode 100644 index 00000000..48c3cdee --- /dev/null +++ b/backend/services/backup-service/src/application/application.module.ts @@ -0,0 +1,19 @@ +import { Module } from '@nestjs/common'; +import { ConfigModule } from '@nestjs/config'; +import { InfrastructureModule } from '../infrastructure/infrastructure.module'; +import { StoreBackupShareHandler } from './commands/store-backup-share/store-backup-share.handler'; +import { RevokeShareHandler } from './commands/revoke-share/revoke-share.handler'; +import { GetBackupShareHandler } from './queries/get-backup-share/get-backup-share.handler'; +import { BackupShareApplicationService } from './services/backup-share-application.service'; + +@Module({ + imports: [ConfigModule, InfrastructureModule], + providers: [ + StoreBackupShareHandler, + RevokeShareHandler, + GetBackupShareHandler, + BackupShareApplicationService, + ], + exports: [BackupShareApplicationService], +}) +export class ApplicationModule {} diff --git a/backend/services/backup-service/src/application/commands/revoke-share/revoke-share.command.ts b/backend/services/backup-service/src/application/commands/revoke-share/revoke-share.command.ts new file mode 100644 index 00000000..efc029d3 --- /dev/null +++ b/backend/services/backup-service/src/application/commands/revoke-share/revoke-share.command.ts @@ -0,0 +1,9 @@ +export class RevokeShareCommand { + constructor( + public readonly userId: string, + public readonly publicKey: string, + public readonly reason: string, + public readonly sourceService: string, + public readonly sourceIp: string, + ) {} +} diff --git a/backend/services/backup-service/src/application/commands/revoke-share/revoke-share.handler.ts b/backend/services/backup-service/src/application/commands/revoke-share/revoke-share.handler.ts new file mode 100644 index 00000000..7591b2fb --- /dev/null +++ b/backend/services/backup-service/src/application/commands/revoke-share/revoke-share.handler.ts @@ -0,0 +1,54 @@ +import { Inject, Injectable, Logger } from '@nestjs/common'; +import { RevokeShareCommand } from './revoke-share.command'; +import { BACKUP_SHARE_REPOSITORY } from '../../../domain'; +import type { BackupShareRepository } from '../../../domain'; +import { AuditLogRepository } from '../../../infrastructure/persistence/repositories/audit-log.repository'; +import { ApplicationError } from '../../errors/application.error'; + +@Injectable() +export class RevokeShareHandler { + private readonly logger = new Logger(RevokeShareHandler.name); + + constructor( + @Inject(BACKUP_SHARE_REPOSITORY) + private readonly repository: BackupShareRepository, + private readonly auditLogRepository: AuditLogRepository, + ) {} + + async execute(command: RevokeShareCommand): Promise { + this.logger.log(`Revoking backup share for user: ${command.userId}`); + + const userId = BigInt(command.userId); + + // Find share + const share = await this.repository.findByUserIdAndPublicKey( + userId, + command.publicKey, + ); + + if (!share) { + throw new ApplicationError( + 'Backup share not found', + 'SHARE_NOT_FOUND', + ); + } + + // Revoke the share + share.revoke(command.reason); + + // Save changes + await this.repository.save(share); + + // Log audit + await this.auditLogRepository.log({ + shareId: share.shareId!, + userId, + action: 'REVOKE', + sourceService: command.sourceService, + sourceIp: command.sourceIp, + success: true, + }); + + this.logger.log(`Backup share revoked: shareId=${share.shareId}, reason=${command.reason}`); + } +} diff --git a/backend/services/backup-service/src/application/commands/store-backup-share/store-backup-share.command.ts b/backend/services/backup-service/src/application/commands/store-backup-share/store-backup-share.command.ts new file mode 100644 index 00000000..a9d165a5 --- /dev/null +++ b/backend/services/backup-service/src/application/commands/store-backup-share/store-backup-share.command.ts @@ -0,0 +1,12 @@ +export class StoreBackupShareCommand { + constructor( + public readonly userId: string, + public readonly accountSequence: number, + public readonly publicKey: string, + public readonly encryptedShareData: string, + public readonly sourceService: string, + public readonly sourceIp: string, + public readonly threshold?: number, + public readonly totalParties?: number, + ) {} +} diff --git a/backend/services/backup-service/src/application/commands/store-backup-share/store-backup-share.handler.ts b/backend/services/backup-service/src/application/commands/store-backup-share/store-backup-share.handler.ts new file mode 100644 index 00000000..fb29d15b --- /dev/null +++ b/backend/services/backup-service/src/application/commands/store-backup-share/store-backup-share.handler.ts @@ -0,0 +1,83 @@ +import { Inject, Injectable, Logger } from '@nestjs/common'; +import { StoreBackupShareCommand } from './store-backup-share.command'; +import { BackupShare, BACKUP_SHARE_REPOSITORY } from '../../../domain'; +import type { BackupShareRepository } from '../../../domain'; +import { AesEncryptionService } from '../../../infrastructure/crypto/aes-encryption.service'; +import { AuditLogRepository } from '../../../infrastructure/persistence/repositories/audit-log.repository'; +import { ApplicationError } from '../../errors/application.error'; + +export interface StoreBackupShareResult { + shareId: string; +} + +@Injectable() +export class StoreBackupShareHandler { + private readonly logger = new Logger(StoreBackupShareHandler.name); + + constructor( + @Inject(BACKUP_SHARE_REPOSITORY) + private readonly repository: BackupShareRepository, + private readonly encryptionService: AesEncryptionService, + private readonly auditLogRepository: AuditLogRepository, + ) {} + + async execute(command: StoreBackupShareCommand): Promise { + this.logger.log(`Storing backup share for user: ${command.userId}`); + + const userId = BigInt(command.userId); + + // Check if share already exists for this user + const existing = await this.repository.findByUserId(userId); + if (existing) { + throw new ApplicationError( + 'Backup share already exists for this user', + 'SHARE_ALREADY_EXISTS', + ); + } + + // Check if share already exists for this public key + const existingByPubKey = await this.repository.findByPublicKey(command.publicKey); + if (existingByPubKey) { + throw new ApplicationError( + 'Backup share already exists for this public key', + 'SHARE_ALREADY_EXISTS', + ); + } + + // Double encryption - the data is already encrypted by identity-service, + // we encrypt it again for defense in depth + const { encrypted, keyId } = await this.encryptionService.encrypt( + command.encryptedShareData, + ); + + // Create domain entity + const share = BackupShare.create({ + userId, + accountSequence: BigInt(command.accountSequence), + publicKey: command.publicKey, + encryptedShareData: encrypted, + encryptionKeyId: keyId, + threshold: command.threshold, + totalParties: command.totalParties, + }); + + // Save to repository + const saved = await this.repository.save(share); + + // Log audit + await this.auditLogRepository.log({ + shareId: saved.shareId!, + userId, + action: 'STORE', + sourceService: command.sourceService, + sourceIp: command.sourceIp, + success: true, + }); + + this.logger.log(`Backup share stored successfully: shareId=${saved.shareId}`); + + return { + shareId: saved.shareId!.toString(), + }; + } +} diff --git a/backend/services/backup-service/src/application/errors/application.error.ts b/backend/services/backup-service/src/application/errors/application.error.ts new file mode 100644 index 00000000..f921d483 --- /dev/null +++ b/backend/services/backup-service/src/application/errors/application.error.ts @@ -0,0 +1,10 @@ +export class ApplicationError extends Error { + constructor( + message: string, + public readonly code: string, + ) { + super(message); + this.name = 'ApplicationError'; + Object.setPrototypeOf(this, ApplicationError.prototype); + } +} diff --git a/backend/services/backup-service/src/application/index.ts b/backend/services/backup-service/src/application/index.ts new file mode 100644 index 00000000..a1af177f --- /dev/null +++ b/backend/services/backup-service/src/application/index.ts @@ -0,0 +1,18 @@ +// Commands +export * from './commands/store-backup-share/store-backup-share.command'; +export * from './commands/store-backup-share/store-backup-share.handler'; +export * from './commands/revoke-share/revoke-share.command'; +export * from './commands/revoke-share/revoke-share.handler'; + +// Queries +export * from './queries/get-backup-share/get-backup-share.query'; +export * from './queries/get-backup-share/get-backup-share.handler'; + +// Services +export * from './services/backup-share-application.service'; + +// Errors +export * from './errors/application.error'; + +// Module +export * from './application.module'; diff --git a/backend/services/backup-service/src/application/queries/get-backup-share/get-backup-share.handler.ts b/backend/services/backup-service/src/application/queries/get-backup-share/get-backup-share.handler.ts new file mode 100644 index 00000000..845f5661 --- /dev/null +++ b/backend/services/backup-service/src/application/queries/get-backup-share/get-backup-share.handler.ts @@ -0,0 +1,120 @@ +import { Inject, Injectable, Logger } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { GetBackupShareQuery } from './get-backup-share.query'; +import { BackupShareStatus, BACKUP_SHARE_REPOSITORY } from '../../../domain'; +import type { BackupShareRepository } from '../../../domain'; +import { AesEncryptionService } from '../../../infrastructure/crypto/aes-encryption.service'; +import { AuditLogRepository } from '../../../infrastructure/persistence/repositories/audit-log.repository'; +import { ApplicationError } from '../../errors/application.error'; + +export interface BackupShareDto { + encryptedShareData: string; + partyIndex: number; + publicKey: string; +} + +@Injectable() +export class GetBackupShareHandler { + private readonly logger = new Logger(GetBackupShareHandler.name); + private readonly maxRetrievesPerDay: number; + + constructor( + @Inject(BACKUP_SHARE_REPOSITORY) + private readonly repository: BackupShareRepository, + private readonly encryptionService: AesEncryptionService, + private readonly auditLogRepository: AuditLogRepository, + private readonly configService: ConfigService, + ) { + this.maxRetrievesPerDay = this.configService.get('MAX_RETRIEVE_PER_DAY') || 3; + } + + async execute(query: GetBackupShareQuery): Promise { + this.logger.log(`Retrieving backup share for user: ${query.userId}`); + + const userId = BigInt(query.userId); + + // Check rate limit + const retrievesToday = await this.auditLogRepository.countRetrievesByUserToday(userId); + if (retrievesToday >= this.maxRetrievesPerDay) { + await this.auditLogRepository.log({ + shareId: 0n, + userId, + action: 'RETRIEVE', + sourceService: query.sourceService, + sourceIp: query.sourceIp, + success: false, + errorMessage: 'Rate limit exceeded', + }); + throw new ApplicationError( + `Rate limit exceeded. Maximum ${this.maxRetrievesPerDay} retrieves per day.`, + 'RATE_LIMIT_EXCEEDED', + ); + } + + // Find share + const share = await this.repository.findByUserIdAndPublicKey( + userId, + query.publicKey, + ); + + if (!share) { + await this.auditLogRepository.log({ + shareId: 0n, + userId, + action: 'RETRIEVE', + sourceService: query.sourceService, + sourceIp: query.sourceIp, + success: false, + errorMessage: 'Share not found', + }); + throw new ApplicationError( + 'Backup share not found', + 'SHARE_NOT_FOUND', + ); + } + + if (share.status !== BackupShareStatus.ACTIVE) { + await this.auditLogRepository.log({ + shareId: share.shareId!, + userId, + action: 'RETRIEVE', + sourceService: query.sourceService, + sourceIp: query.sourceIp, + success: false, + errorMessage: `Share is ${share.status}`, + }); + throw new ApplicationError( + 'Backup share is not active', + 'SHARE_NOT_ACTIVE', + ); + } + + // Record access + share.recordAccess(); + await this.repository.save(share); + + // Decrypt the data (removes our encryption layer, returns identity-service encrypted data) + const decrypted = await this.encryptionService.decrypt( + share.encryptedShareData, + share.encryptionKeyId, + ); + + // Log audit + await this.auditLogRepository.log({ + shareId: share.shareId!, + userId, + action: 'RETRIEVE', + sourceService: query.sourceService, + sourceIp: query.sourceIp, + success: true, + }); + + this.logger.log(`Backup share retrieved: shareId=${share.shareId}`); + + return { + encryptedShareData: decrypted, + partyIndex: share.partyIndex, + publicKey: share.publicKey, + }; + } +} diff --git a/backend/services/backup-service/src/application/queries/get-backup-share/get-backup-share.query.ts b/backend/services/backup-service/src/application/queries/get-backup-share/get-backup-share.query.ts new file mode 100644 index 00000000..369baf23 --- /dev/null +++ b/backend/services/backup-service/src/application/queries/get-backup-share/get-backup-share.query.ts @@ -0,0 +1,10 @@ +export class GetBackupShareQuery { + constructor( + public readonly userId: string, + public readonly publicKey: string, + public readonly recoveryToken: string, + public readonly sourceService: string, + public readonly sourceIp: string, + public readonly deviceId?: string, + ) {} +} diff --git a/backend/services/backup-service/src/application/services/backup-share-application.service.ts b/backend/services/backup-service/src/application/services/backup-share-application.service.ts new file mode 100644 index 00000000..1fed59f7 --- /dev/null +++ b/backend/services/backup-service/src/application/services/backup-share-application.service.ts @@ -0,0 +1,28 @@ +import { Injectable } from '@nestjs/common'; +import { StoreBackupShareCommand } from '../commands/store-backup-share/store-backup-share.command'; +import { StoreBackupShareHandler, StoreBackupShareResult } from '../commands/store-backup-share/store-backup-share.handler'; +import { RevokeShareCommand } from '../commands/revoke-share/revoke-share.command'; +import { RevokeShareHandler } from '../commands/revoke-share/revoke-share.handler'; +import { GetBackupShareQuery } from '../queries/get-backup-share/get-backup-share.query'; +import { GetBackupShareHandler, BackupShareDto } from '../queries/get-backup-share/get-backup-share.handler'; + +@Injectable() +export class BackupShareApplicationService { + constructor( + private readonly storeHandler: StoreBackupShareHandler, + private readonly revokeHandler: RevokeShareHandler, + private readonly getHandler: GetBackupShareHandler, + ) {} + + async storeBackupShare(command: StoreBackupShareCommand): Promise { + return this.storeHandler.execute(command); + } + + async revokeShare(command: RevokeShareCommand): Promise { + return this.revokeHandler.execute(command); + } + + async getBackupShare(query: GetBackupShareQuery): Promise { + return this.getHandler.execute(query); + } +} diff --git a/backend/services/backup-service/src/config/index.ts b/backend/services/backup-service/src/config/index.ts new file mode 100644 index 00000000..a7ba45aa --- /dev/null +++ b/backend/services/backup-service/src/config/index.ts @@ -0,0 +1,30 @@ +export default () => ({ + app: { + port: parseInt(process.env.APP_PORT || '3002', 10), + env: process.env.APP_ENV || 'development', + }, + database: { + url: process.env.DATABASE_URL, + }, + security: { + serviceJwtSecret: process.env.SERVICE_JWT_SECRET, + allowedServices: (process.env.ALLOWED_SERVICES || 'identity-service,recovery-service') + .split(',') + .map((s) => s.trim()), + }, + encryption: { + key: process.env.BACKUP_ENCRYPTION_KEY, + keyId: process.env.BACKUP_ENCRYPTION_KEY_ID || 'key-v1', + }, + rateLimit: { + maxRetrievePerDay: parseInt(process.env.MAX_RETRIEVE_PER_DAY || '3', 10), + maxStorePerMinute: parseInt(process.env.MAX_STORE_PER_MINUTE || '10', 10), + }, + audit: { + logRetentionDays: parseInt(process.env.AUDIT_LOG_RETENTION_DAYS || '365', 10), + }, + monitoring: { + prometheusEnabled: process.env.PROMETHEUS_ENABLED === 'true', + prometheusPort: parseInt(process.env.PROMETHEUS_PORT || '9102', 10), + }, +}); diff --git a/backend/services/backup-service/src/domain/domain.module.ts b/backend/services/backup-service/src/domain/domain.module.ts new file mode 100644 index 00000000..f686c30b --- /dev/null +++ b/backend/services/backup-service/src/domain/domain.module.ts @@ -0,0 +1,8 @@ +import { Module } from '@nestjs/common'; + +@Module({ + imports: [], + providers: [], + exports: [], +}) +export class DomainModule {} diff --git a/backend/services/backup-service/src/domain/entities/backup-share.entity.ts b/backend/services/backup-service/src/domain/entities/backup-share.entity.ts new file mode 100644 index 00000000..21f2ca8e --- /dev/null +++ b/backend/services/backup-service/src/domain/entities/backup-share.entity.ts @@ -0,0 +1,201 @@ +import { DomainError } from '../errors/domain.error'; + +export enum BackupShareStatus { + ACTIVE = 'ACTIVE', + REVOKED = 'REVOKED', + ROTATED = 'ROTATED', +} + +export interface BackupShareProps { + shareId: bigint | null; + userId: bigint; + accountSequence: bigint; + publicKey: string; + partyIndex: number; + threshold: number; + totalParties: number; + encryptedShareData: string; + encryptionKeyId: string; + status: BackupShareStatus; + accessCount: number; + lastAccessedAt: Date | null; + createdAt: Date; + updatedAt: Date; + revokedAt: Date | null; +} + +export class BackupShare { + private _shareId: bigint | null; + private readonly _userId: bigint; + private readonly _accountSequence: bigint; + private readonly _publicKey: string; + private readonly _partyIndex: number; + private readonly _threshold: number; + private readonly _totalParties: number; + private _encryptedShareData: string; + private _encryptionKeyId: string; + private _status: BackupShareStatus; + private _accessCount: number; + private _lastAccessedAt: Date | null; + private readonly _createdAt: Date; + private _updatedAt: Date; + private _revokedAt: Date | null; + + private constructor(props: BackupShareProps) { + this._shareId = props.shareId; + this._userId = props.userId; + this._accountSequence = props.accountSequence; + this._publicKey = props.publicKey; + this._partyIndex = props.partyIndex; + this._threshold = props.threshold; + this._totalParties = props.totalParties; + this._encryptedShareData = props.encryptedShareData; + this._encryptionKeyId = props.encryptionKeyId; + this._status = props.status; + this._accessCount = props.accessCount; + this._lastAccessedAt = props.lastAccessedAt; + this._createdAt = props.createdAt; + this._updatedAt = props.updatedAt; + this._revokedAt = props.revokedAt; + } + + static create(params: { + userId: bigint; + accountSequence: bigint; + publicKey: string; + encryptedShareData: string; + encryptionKeyId: string; + threshold?: number; + totalParties?: number; + }): BackupShare { + const now = new Date(); + return new BackupShare({ + shareId: null, + userId: params.userId, + accountSequence: params.accountSequence, + publicKey: params.publicKey, + partyIndex: 2, // Backup = Party 2 + threshold: params.threshold ?? 2, + totalParties: params.totalParties ?? 3, + encryptedShareData: params.encryptedShareData, + encryptionKeyId: params.encryptionKeyId, + status: BackupShareStatus.ACTIVE, + accessCount: 0, + lastAccessedAt: null, + createdAt: now, + updatedAt: now, + revokedAt: null, + }); + } + + static reconstitute(props: BackupShareProps): BackupShare { + return new BackupShare(props); + } + + recordAccess(): void { + if (this._status !== BackupShareStatus.ACTIVE) { + throw new DomainError('Cannot access revoked or rotated share'); + } + this._accessCount++; + this._lastAccessedAt = new Date(); + this._updatedAt = new Date(); + } + + revoke(reason: string): void { + if (this._status === BackupShareStatus.REVOKED) { + throw new DomainError('Share already revoked'); + } + this._status = BackupShareStatus.REVOKED; + this._revokedAt = new Date(); + this._updatedAt = new Date(); + } + + rotate(newEncryptedData: string, newKeyId: string): void { + if (this._status === BackupShareStatus.REVOKED) { + throw new DomainError('Cannot rotate revoked share'); + } + this._encryptedShareData = newEncryptedData; + this._encryptionKeyId = newKeyId; + this._status = BackupShareStatus.ACTIVE; + this._updatedAt = new Date(); + } + + isActive(): boolean { + return this._status === BackupShareStatus.ACTIVE; + } + + // Getters + get shareId(): bigint | null { + return this._shareId; + } + get userId(): bigint { + return this._userId; + } + get accountSequence(): bigint { + return this._accountSequence; + } + get publicKey(): string { + return this._publicKey; + } + get partyIndex(): number { + return this._partyIndex; + } + get threshold(): number { + return this._threshold; + } + get totalParties(): number { + return this._totalParties; + } + get encryptedShareData(): string { + return this._encryptedShareData; + } + get encryptionKeyId(): string { + return this._encryptionKeyId; + } + get status(): BackupShareStatus { + return this._status; + } + get accessCount(): number { + return this._accessCount; + } + get lastAccessedAt(): Date | null { + return this._lastAccessedAt; + } + get createdAt(): Date { + return this._createdAt; + } + get updatedAt(): Date { + return this._updatedAt; + } + get revokedAt(): Date | null { + return this._revokedAt; + } + + // For persistence + setShareId(id: bigint): void { + if (this._shareId !== null) { + throw new DomainError('Share ID already set'); + } + this._shareId = id; + } + + toProps(): BackupShareProps { + return { + shareId: this._shareId, + userId: this._userId, + accountSequence: this._accountSequence, + publicKey: this._publicKey, + partyIndex: this._partyIndex, + threshold: this._threshold, + totalParties: this._totalParties, + encryptedShareData: this._encryptedShareData, + encryptionKeyId: this._encryptionKeyId, + status: this._status, + accessCount: this._accessCount, + lastAccessedAt: this._lastAccessedAt, + createdAt: this._createdAt, + updatedAt: this._updatedAt, + revokedAt: this._revokedAt, + }; + } +} diff --git a/backend/services/backup-service/src/domain/errors/domain.error.ts b/backend/services/backup-service/src/domain/errors/domain.error.ts new file mode 100644 index 00000000..c4ec85f2 --- /dev/null +++ b/backend/services/backup-service/src/domain/errors/domain.error.ts @@ -0,0 +1,7 @@ +export class DomainError extends Error { + constructor(message: string) { + super(message); + this.name = 'DomainError'; + Object.setPrototypeOf(this, DomainError.prototype); + } +} diff --git a/backend/services/backup-service/src/domain/index.ts b/backend/services/backup-service/src/domain/index.ts new file mode 100644 index 00000000..8564cb48 --- /dev/null +++ b/backend/services/backup-service/src/domain/index.ts @@ -0,0 +1,15 @@ +// Entities +export * from './entities/backup-share.entity'; + +// Value Objects +export * from './value-objects/share-id.vo'; +export * from './value-objects/encrypted-data.vo'; + +// Repositories +export * from './repositories/backup-share.repository.interface'; + +// Errors +export * from './errors/domain.error'; + +// Module +export * from './domain.module'; diff --git a/backend/services/backup-service/src/domain/repositories/backup-share.repository.interface.ts b/backend/services/backup-service/src/domain/repositories/backup-share.repository.interface.ts new file mode 100644 index 00000000..a289fd8a --- /dev/null +++ b/backend/services/backup-service/src/domain/repositories/backup-share.repository.interface.ts @@ -0,0 +1,16 @@ +import { BackupShare } from '../entities/backup-share.entity'; + +export const BACKUP_SHARE_REPOSITORY = Symbol('BACKUP_SHARE_REPOSITORY'); + +export interface BackupShareRepository { + save(share: BackupShare): Promise; + findById(shareId: bigint): Promise; + findByUserId(userId: bigint): Promise; + findByPublicKey(publicKey: string): Promise; + findByUserIdAndPublicKey( + userId: bigint, + publicKey: string, + ): Promise; + findByAccountSequence(accountSequence: bigint): Promise; + delete(shareId: bigint): Promise; +} diff --git a/backend/services/backup-service/src/domain/value-objects/encrypted-data.vo.ts b/backend/services/backup-service/src/domain/value-objects/encrypted-data.vo.ts new file mode 100644 index 00000000..2f001892 --- /dev/null +++ b/backend/services/backup-service/src/domain/value-objects/encrypted-data.vo.ts @@ -0,0 +1,74 @@ +import { DomainError } from '../errors/domain.error'; + +export class EncryptedData { + private readonly _ciphertext: string; + private readonly _iv: string; + private readonly _authTag: string; + private readonly _keyId: string; + + private constructor( + ciphertext: string, + iv: string, + authTag: string, + keyId: string, + ) { + this._ciphertext = ciphertext; + this._iv = iv; + this._authTag = authTag; + this._keyId = keyId; + } + + static create(params: { + ciphertext: string; + iv: string; + authTag: string; + keyId: string; + }): EncryptedData { + if (!params.ciphertext || params.ciphertext.length === 0) { + throw new DomainError('Ciphertext cannot be empty'); + } + if (!params.iv || params.iv.length === 0) { + throw new DomainError('IV cannot be empty'); + } + if (!params.authTag || params.authTag.length === 0) { + throw new DomainError('Auth tag cannot be empty'); + } + if (!params.keyId || params.keyId.length === 0) { + throw new DomainError('Key ID cannot be empty'); + } + return new EncryptedData( + params.ciphertext, + params.iv, + params.authTag, + params.keyId, + ); + } + + static fromSerializedString(serialized: string, keyId: string): EncryptedData { + const parts = serialized.split(':'); + if (parts.length !== 3) { + throw new DomainError('Invalid encrypted data format'); + } + return new EncryptedData(parts[0], parts[1], parts[2], keyId); + } + + get ciphertext(): string { + return this._ciphertext; + } + + get iv(): string { + return this._iv; + } + + get authTag(): string { + return this._authTag; + } + + get keyId(): string { + return this._keyId; + } + + toSerializedString(): string { + return `${this._ciphertext}:${this._iv}:${this._authTag}`; + } +} diff --git a/backend/services/backup-service/src/domain/value-objects/share-id.vo.ts b/backend/services/backup-service/src/domain/value-objects/share-id.vo.ts new file mode 100644 index 00000000..f3831b6d --- /dev/null +++ b/backend/services/backup-service/src/domain/value-objects/share-id.vo.ts @@ -0,0 +1,29 @@ +import { DomainError } from '../errors/domain.error'; + +export class ShareId { + private readonly _value: bigint; + + private constructor(value: bigint) { + this._value = value; + } + + static create(value: bigint | string | number): ShareId { + const bigIntValue = typeof value === 'bigint' ? value : BigInt(value); + if (bigIntValue <= 0n) { + throw new DomainError('ShareId must be a positive number'); + } + return new ShareId(bigIntValue); + } + + get value(): bigint { + return this._value; + } + + toString(): string { + return this._value.toString(); + } + + equals(other: ShareId): boolean { + return this._value === other._value; + } +} diff --git a/backend/services/backup-service/src/infrastructure/crypto/aes-encryption.service.ts b/backend/services/backup-service/src/infrastructure/crypto/aes-encryption.service.ts new file mode 100644 index 00000000..26b83ead --- /dev/null +++ b/backend/services/backup-service/src/infrastructure/crypto/aes-encryption.service.ts @@ -0,0 +1,106 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import * as crypto from 'crypto'; + +export interface EncryptionResult { + encrypted: string; + keyId: string; +} + +@Injectable() +export class AesEncryptionService { + private readonly logger = new Logger(AesEncryptionService.name); + private readonly algorithm = 'aes-256-gcm'; + private readonly keyMap: Map = new Map(); + private readonly currentKeyId: string; + + constructor(private readonly configService: ConfigService) { + // Load encryption keys + const encryptionKey = this.configService.get('BACKUP_ENCRYPTION_KEY'); + const keyId = this.configService.get('BACKUP_ENCRYPTION_KEY_ID') || 'key-v1'; + + if (!encryptionKey) { + throw new Error('BACKUP_ENCRYPTION_KEY is not configured'); + } + + // Convert hex string to buffer + const keyBuffer = Buffer.from(encryptionKey, 'hex'); + if (keyBuffer.length !== 32) { + throw new Error('BACKUP_ENCRYPTION_KEY must be 256 bits (64 hex characters)'); + } + + this.keyMap.set(keyId, keyBuffer); + this.currentKeyId = keyId; + + this.logger.log(`AES encryption service initialized with key: ${keyId}`); + } + + async encrypt(plaintext: string): Promise { + const key = this.keyMap.get(this.currentKeyId); + if (!key) { + throw new Error(`Encryption key not found: ${this.currentKeyId}`); + } + + // Generate random IV (12 bytes for GCM) + const iv = crypto.randomBytes(12); + + // Create cipher + const cipher = crypto.createCipheriv(this.algorithm, key, iv); + + // Encrypt + let encrypted = cipher.update(plaintext, 'utf8', 'base64'); + encrypted += cipher.final('base64'); + + // Get auth tag + const authTag = cipher.getAuthTag(); + + // Combine: base64(ciphertext):base64(iv):base64(authTag) + const combined = `${encrypted}:${iv.toString('base64')}:${authTag.toString('base64')}`; + + return { + encrypted: combined, + keyId: this.currentKeyId, + }; + } + + async decrypt(encryptedData: string, keyId: string): Promise { + const key = this.keyMap.get(keyId); + if (!key) { + throw new Error(`Decryption key not found: ${keyId}`); + } + + // Parse combined string + const parts = encryptedData.split(':'); + if (parts.length !== 3) { + throw new Error('Invalid encrypted data format'); + } + + const [ciphertext, ivBase64, authTagBase64] = parts; + const iv = Buffer.from(ivBase64, 'base64'); + const authTag = Buffer.from(authTagBase64, 'base64'); + + // Create decipher + const decipher = crypto.createDecipheriv(this.algorithm, key, iv); + decipher.setAuthTag(authTag); + + // Decrypt + let decrypted = decipher.update(ciphertext, 'base64', 'utf8'); + decrypted += decipher.final('utf8'); + + return decrypted; + } + + // For key rotation - add a new key + addKey(keyId: string, keyHex: string): void { + const keyBuffer = Buffer.from(keyHex, 'hex'); + if (keyBuffer.length !== 32) { + throw new Error('Key must be 256 bits (64 hex characters)'); + } + this.keyMap.set(keyId, keyBuffer); + this.logger.log(`Added encryption key: ${keyId}`); + } + + getCurrentKeyId(): string { + return this.currentKeyId; + } +} diff --git a/backend/services/backup-service/src/infrastructure/index.ts b/backend/services/backup-service/src/infrastructure/index.ts new file mode 100644 index 00000000..a213ccf4 --- /dev/null +++ b/backend/services/backup-service/src/infrastructure/index.ts @@ -0,0 +1,10 @@ +// Persistence +export * from './persistence/prisma/prisma.service'; +export * from './persistence/repositories/backup-share.repository.impl'; +export * from './persistence/repositories/audit-log.repository'; + +// Crypto +export * from './crypto/aes-encryption.service'; + +// Module +export * from './infrastructure.module'; diff --git a/backend/services/backup-service/src/infrastructure/infrastructure.module.ts b/backend/services/backup-service/src/infrastructure/infrastructure.module.ts new file mode 100644 index 00000000..2c76bd5a --- /dev/null +++ b/backend/services/backup-service/src/infrastructure/infrastructure.module.ts @@ -0,0 +1,27 @@ +import { Module } from '@nestjs/common'; +import { ConfigModule } from '@nestjs/config'; +import { PrismaService } from './persistence/prisma/prisma.service'; +import { BackupShareRepositoryImpl } from './persistence/repositories/backup-share.repository.impl'; +import { AuditLogRepository } from './persistence/repositories/audit-log.repository'; +import { AesEncryptionService } from './crypto/aes-encryption.service'; +import { BACKUP_SHARE_REPOSITORY } from '../domain'; + +@Module({ + imports: [ConfigModule], + providers: [ + PrismaService, + AuditLogRepository, + AesEncryptionService, + { + provide: BACKUP_SHARE_REPOSITORY, + useClass: BackupShareRepositoryImpl, + }, + ], + exports: [ + PrismaService, + AuditLogRepository, + AesEncryptionService, + BACKUP_SHARE_REPOSITORY, + ], +}) +export class InfrastructureModule {} diff --git a/backend/services/backup-service/src/infrastructure/persistence/prisma/prisma.service.ts b/backend/services/backup-service/src/infrastructure/persistence/prisma/prisma.service.ts new file mode 100644 index 00000000..44a590ab --- /dev/null +++ b/backend/services/backup-service/src/infrastructure/persistence/prisma/prisma.service.ts @@ -0,0 +1,52 @@ +import { Injectable, OnModuleInit, OnModuleDestroy, Logger } from '@nestjs/common'; +import { PrismaClient } from '@prisma/client'; +import { PrismaPg } from '@prisma/adapter-pg'; +import { Pool } from 'pg'; + +@Injectable() +export class PrismaService + extends PrismaClient + implements OnModuleInit, OnModuleDestroy +{ + private readonly logger = new Logger(PrismaService.name); + private pool: Pool; + + constructor() { + const connectionString = process.env.DATABASE_URL; + const pool = new Pool({ connectionString }); + const adapter = new PrismaPg(pool); + + super({ + adapter, + log: [ + { emit: 'event', level: 'query' }, + { emit: 'stdout', level: 'info' }, + { emit: 'stdout', level: 'warn' }, + { emit: 'stdout', level: 'error' }, + ], + }); + + this.pool = pool; + } + + async onModuleInit() { + this.logger.log('Connecting to database...'); + await this.$connect(); + this.logger.log('Database connected successfully'); + } + + async onModuleDestroy() { + this.logger.log('Disconnecting from database...'); + await this.$disconnect(); + await this.pool.end(); + this.logger.log('Database disconnected'); + } + + async cleanDatabase() { + if (process.env.APP_ENV !== 'test') { + throw new Error('cleanDatabase is only available in test environment'); + } + await this.shareAccessLog.deleteMany(); + await this.backupShare.deleteMany(); + } +} diff --git a/backend/services/backup-service/src/infrastructure/persistence/repositories/audit-log.repository.ts b/backend/services/backup-service/src/infrastructure/persistence/repositories/audit-log.repository.ts new file mode 100644 index 00000000..94f28960 --- /dev/null +++ b/backend/services/backup-service/src/infrastructure/persistence/repositories/audit-log.repository.ts @@ -0,0 +1,70 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { PrismaService } from '../prisma/prisma.service'; + +export interface AuditLogEntry { + shareId: bigint; + userId: bigint; + action: 'STORE' | 'RETRIEVE' | 'REVOKE' | 'ROTATE'; + sourceService: string; + sourceIp: string; + success: boolean; + errorMessage?: string; +} + +@Injectable() +export class AuditLogRepository { + private readonly logger = new Logger(AuditLogRepository.name); + + constructor(private readonly prisma: PrismaService) {} + + async log(entry: AuditLogEntry): Promise { + try { + await this.prisma.shareAccessLog.create({ + data: { + shareId: entry.shareId, + userId: entry.userId, + action: entry.action, + sourceService: entry.sourceService, + sourceIp: entry.sourceIp, + success: entry.success, + errorMessage: entry.errorMessage, + }, + }); + this.logger.debug( + `Audit log created: ${entry.action} for share ${entry.shareId}`, + ); + } catch (error) { + this.logger.error(`Failed to create audit log: ${error.message}`); + // Don't throw - audit log failures shouldn't affect main operations + } + } + + async findByShareId(shareId: bigint, limit = 100) { + return this.prisma.shareAccessLog.findMany({ + where: { shareId }, + orderBy: { createdAt: 'desc' }, + take: limit, + }); + } + + async findByUserId(userId: bigint, limit = 100) { + return this.prisma.shareAccessLog.findMany({ + where: { userId }, + orderBy: { createdAt: 'desc' }, + take: limit, + }); + } + + async countRetrievesByUserToday(userId: bigint): Promise { + const startOfDay = new Date(); + startOfDay.setHours(0, 0, 0, 0); + + return this.prisma.shareAccessLog.count({ + where: { + userId, + action: 'RETRIEVE', + createdAt: { gte: startOfDay }, + }, + }); + } +} diff --git a/backend/services/backup-service/src/infrastructure/persistence/repositories/backup-share.repository.impl.ts b/backend/services/backup-service/src/infrastructure/persistence/repositories/backup-share.repository.impl.ts new file mode 100644 index 00000000..54b80a4d --- /dev/null +++ b/backend/services/backup-service/src/infrastructure/persistence/repositories/backup-share.repository.impl.ts @@ -0,0 +1,123 @@ +import { Injectable } from '@nestjs/common'; +import { PrismaService } from '../prisma/prisma.service'; +import { + BackupShare, + BackupShareStatus, + BackupShareProps, + BackupShareRepository, +} from '../../../domain'; +import { BackupShare as PrismaBackupShare } from '@prisma/client'; + +@Injectable() +export class BackupShareRepositoryImpl implements BackupShareRepository { + constructor(private readonly prisma: PrismaService) {} + + async save(share: BackupShare): Promise { + const props = share.toProps(); + + if (props.shareId === null) { + // Create new + const created = await this.prisma.backupShare.create({ + data: { + userId: props.userId, + accountSequence: props.accountSequence, + publicKey: props.publicKey, + partyIndex: props.partyIndex, + threshold: props.threshold, + totalParties: props.totalParties, + encryptedShareData: props.encryptedShareData, + encryptionKeyId: props.encryptionKeyId, + status: props.status, + accessCount: props.accessCount, + lastAccessedAt: props.lastAccessedAt, + revokedAt: props.revokedAt, + }, + }); + return this.toDomain(created); + } else { + // Update existing + const updated = await this.prisma.backupShare.update({ + where: { shareId: props.shareId }, + data: { + encryptedShareData: props.encryptedShareData, + encryptionKeyId: props.encryptionKeyId, + status: props.status, + accessCount: props.accessCount, + lastAccessedAt: props.lastAccessedAt, + revokedAt: props.revokedAt, + }, + }); + return this.toDomain(updated); + } + } + + async findById(shareId: bigint): Promise { + const record = await this.prisma.backupShare.findUnique({ + where: { shareId }, + }); + return record ? this.toDomain(record) : null; + } + + async findByUserId(userId: bigint): Promise { + const record = await this.prisma.backupShare.findUnique({ + where: { userId }, + }); + return record ? this.toDomain(record) : null; + } + + async findByPublicKey(publicKey: string): Promise { + const record = await this.prisma.backupShare.findUnique({ + where: { publicKey }, + }); + return record ? this.toDomain(record) : null; + } + + async findByUserIdAndPublicKey( + userId: bigint, + publicKey: string, + ): Promise { + const record = await this.prisma.backupShare.findFirst({ + where: { + userId, + publicKey, + }, + }); + return record ? this.toDomain(record) : null; + } + + async findByAccountSequence( + accountSequence: bigint, + ): Promise { + const record = await this.prisma.backupShare.findUnique({ + where: { accountSequence }, + }); + return record ? this.toDomain(record) : null; + } + + async delete(shareId: bigint): Promise { + await this.prisma.backupShare.delete({ + where: { shareId }, + }); + } + + private toDomain(record: PrismaBackupShare): BackupShare { + const props: BackupShareProps = { + shareId: record.shareId, + userId: record.userId, + accountSequence: record.accountSequence, + publicKey: record.publicKey, + partyIndex: record.partyIndex, + threshold: record.threshold, + totalParties: record.totalParties, + encryptedShareData: record.encryptedShareData, + encryptionKeyId: record.encryptionKeyId, + status: record.status as BackupShareStatus, + accessCount: record.accessCount, + lastAccessedAt: record.lastAccessedAt, + createdAt: record.createdAt, + updatedAt: record.updatedAt, + revokedAt: record.revokedAt, + }; + return BackupShare.reconstitute(props); + } +} diff --git a/backend/services/backup-service/src/main.ts b/backend/services/backup-service/src/main.ts new file mode 100644 index 00000000..8279f6ad --- /dev/null +++ b/backend/services/backup-service/src/main.ts @@ -0,0 +1,40 @@ +import { NestFactory } from '@nestjs/core'; +import { ValidationPipe, Logger } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { AppModule } from './app.module'; + +async function bootstrap() { + const logger = new Logger('Bootstrap'); + + const app = await NestFactory.create(AppModule, { + logger: ['error', 'warn', 'log', 'debug'], + }); + + // Global validation pipe + app.useGlobalPipes( + new ValidationPipe({ + whitelist: true, + forbidNonWhitelisted: true, + transform: true, + transformOptions: { + enableImplicitConversion: true, + }, + }), + ); + + // CORS - only allow internal services + app.enableCors({ + origin: false, // Disable public CORS - only internal services should access + }); + + const configService = app.get(ConfigService); + const port = configService.get('app.port') || 3002; + const env = configService.get('app.env') || 'development'; + + await app.listen(port); + + logger.log(`Backup Service is running on port ${port} in ${env} mode`); + logger.log(`Health check: http://localhost:${port}/health`); +} + +bootstrap(); diff --git a/backend/services/backup-service/src/shared/filters/global-exception.filter.ts b/backend/services/backup-service/src/shared/filters/global-exception.filter.ts new file mode 100644 index 00000000..d471488a --- /dev/null +++ b/backend/services/backup-service/src/shared/filters/global-exception.filter.ts @@ -0,0 +1,92 @@ +import { + ExceptionFilter, + Catch, + ArgumentsHost, + HttpException, + HttpStatus, + Logger, +} from '@nestjs/common'; +import { Response } from 'express'; +import { ApplicationError } from '../../application/errors/application.error'; +import { DomainError } from '../../domain/errors/domain.error'; + +@Catch() +export class GlobalExceptionFilter implements ExceptionFilter { + private readonly logger = new Logger(GlobalExceptionFilter.name); + + catch(exception: unknown, host: ArgumentsHost) { + const ctx = host.switchToHttp(); + const response = ctx.getResponse(); + const request = ctx.getRequest(); + + let status = HttpStatus.INTERNAL_SERVER_ERROR; + let message = 'Internal server error'; + let code = 'INTERNAL_ERROR'; + + if (exception instanceof HttpException) { + status = exception.getStatus(); + const exceptionResponse = exception.getResponse(); + message = typeof exceptionResponse === 'string' + ? exceptionResponse + : (exceptionResponse as any).message || exception.message; + code = this.getCodeFromStatus(status); + } else if (exception instanceof ApplicationError) { + status = this.getStatusFromCode(exception.code); + message = exception.message; + code = exception.code; + } else if (exception instanceof DomainError) { + status = HttpStatus.BAD_REQUEST; + message = exception.message; + code = 'DOMAIN_ERROR'; + } else if (exception instanceof Error) { + message = exception.message; + } + + this.logger.error( + `${request.method} ${request.url} - ${status} - ${message}`, + exception instanceof Error ? exception.stack : undefined, + ); + + response.status(status).json({ + success: false, + error: message, + code, + timestamp: new Date().toISOString(), + path: request.url, + }); + } + + private getCodeFromStatus(status: number): string { + switch (status) { + case HttpStatus.BAD_REQUEST: + return 'BAD_REQUEST'; + case HttpStatus.UNAUTHORIZED: + return 'UNAUTHORIZED'; + case HttpStatus.FORBIDDEN: + return 'FORBIDDEN'; + case HttpStatus.NOT_FOUND: + return 'NOT_FOUND'; + case HttpStatus.TOO_MANY_REQUESTS: + return 'RATE_LIMIT_EXCEEDED'; + default: + return 'INTERNAL_ERROR'; + } + } + + private getStatusFromCode(code: string): number { + switch (code) { + case 'SHARE_NOT_FOUND': + return HttpStatus.NOT_FOUND; + case 'SHARE_ALREADY_EXISTS': + return HttpStatus.CONFLICT; + case 'SHARE_NOT_ACTIVE': + return HttpStatus.BAD_REQUEST; + case 'RATE_LIMIT_EXCEEDED': + return HttpStatus.TOO_MANY_REQUESTS; + case 'UNAUTHORIZED': + return HttpStatus.UNAUTHORIZED; + default: + return HttpStatus.BAD_REQUEST; + } + } +} diff --git a/backend/services/backup-service/src/shared/guards/service-auth.guard.ts b/backend/services/backup-service/src/shared/guards/service-auth.guard.ts new file mode 100644 index 00000000..55aeaeae --- /dev/null +++ b/backend/services/backup-service/src/shared/guards/service-auth.guard.ts @@ -0,0 +1,70 @@ +import { + Injectable, + CanActivate, + ExecutionContext, + UnauthorizedException, + Logger, +} from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import * as jwt from 'jsonwebtoken'; + +interface ServiceTokenPayload { + service: string; + iat: number; + exp?: number; +} + +@Injectable() +export class ServiceAuthGuard implements CanActivate { + private readonly logger = new Logger(ServiceAuthGuard.name); + private readonly allowedServices: string[]; + private readonly secret: string; + + constructor(private readonly configService: ConfigService) { + this.secret = this.configService.get('SERVICE_JWT_SECRET') || ''; + const allowedServicesStr = this.configService.get('ALLOWED_SERVICES') || 'identity-service,recovery-service'; + this.allowedServices = allowedServicesStr.split(',').map(s => s.trim()); + } + + canActivate(context: ExecutionContext): boolean { + const request = context.switchToHttp().getRequest(); + const serviceToken = request.headers['x-service-token']; + + if (!serviceToken) { + this.logger.warn('Missing service token in request'); + throw new UnauthorizedException('Missing service token'); + } + + try { + const payload = jwt.verify(serviceToken, this.secret) as ServiceTokenPayload; + + if (!this.allowedServices.includes(payload.service)) { + this.logger.warn(`Service not authorized: ${payload.service}`); + throw new UnauthorizedException('Service not authorized'); + } + + // Attach service info to request for audit logging + request.sourceService = payload.service; + request.sourceIp = this.getClientIp(request); + + this.logger.debug(`Authenticated request from service: ${payload.service}`); + return true; + } catch (error) { + if (error instanceof UnauthorizedException) { + throw error; + } + this.logger.warn(`Invalid service token: ${error.message}`); + throw new UnauthorizedException('Invalid service token'); + } + } + + private getClientIp(request: any): string { + return ( + request.headers['x-forwarded-for']?.split(',')[0]?.trim() || + request.headers['x-real-ip'] || + request.connection?.remoteAddress || + request.socket?.remoteAddress || + 'unknown' + ); + } +} diff --git a/backend/services/backup-service/src/shared/interceptors/audit-log.interceptor.ts b/backend/services/backup-service/src/shared/interceptors/audit-log.interceptor.ts new file mode 100644 index 00000000..5d68e08c --- /dev/null +++ b/backend/services/backup-service/src/shared/interceptors/audit-log.interceptor.ts @@ -0,0 +1,63 @@ +import { + Injectable, + NestInterceptor, + ExecutionContext, + CallHandler, + Logger, +} from '@nestjs/common'; +import { Observable } from 'rxjs'; +import { tap } from 'rxjs/operators'; + +@Injectable() +export class AuditLogInterceptor implements NestInterceptor { + private readonly logger = new Logger(AuditLogInterceptor.name); + + intercept(context: ExecutionContext, next: CallHandler): Observable { + const request = context.switchToHttp().getRequest(); + const { method, url, body } = request; + const sourceService = request.sourceService || 'unknown'; + const sourceIp = request.sourceIp || 'unknown'; + + const startTime = Date.now(); + + // Log sensitive fields redacted + const sanitizedBody = this.sanitizeBody(body); + + this.logger.log( + `[${sourceService}] ${method} ${url} - Request from ${sourceIp}`, + ); + this.logger.debug(`Request body: ${JSON.stringify(sanitizedBody)}`); + + return next.handle().pipe( + tap({ + next: (response) => { + const duration = Date.now() - startTime; + this.logger.log( + `[${sourceService}] ${method} ${url} - Response ${duration}ms - Success`, + ); + }, + error: (error) => { + const duration = Date.now() - startTime; + this.logger.error( + `[${sourceService}] ${method} ${url} - Response ${duration}ms - Error: ${error.message}`, + ); + }, + }), + ); + } + + private sanitizeBody(body: any): any { + if (!body) return body; + + const sanitized = { ...body }; + const sensitiveFields = ['encryptedShareData', 'recoveryToken', 'password', 'secret']; + + for (const field of sensitiveFields) { + if (sanitized[field]) { + sanitized[field] = '[REDACTED]'; + } + } + + return sanitized; + } +} diff --git a/backend/services/backup-service/test/README.md b/backend/services/backup-service/test/README.md new file mode 100644 index 00000000..1816f290 --- /dev/null +++ b/backend/services/backup-service/test/README.md @@ -0,0 +1,132 @@ +# Backup Service Tests + +## Test Structure + +``` +test/ +├── unit/ # Unit tests (no external dependencies) +│ ├── api/ # Controller tests +│ ├── application/ # Handler tests +│ ├── domain/ # Entity and value object tests +│ ├── infrastructure/ # Service tests +│ └── shared/ # Guard, filter, interceptor tests +├── integration/ # Integration tests (mocked DB) +├── e2e/ # End-to-end tests +│ ├── backup-share-mock.e2e-spec.ts # Mock DB E2E tests +│ └── backup-share.e2e-spec.ts # Real DB E2E tests +├── setup/ # Test setup files +│ ├── global-setup.ts # DB container setup +│ ├── global-teardown.ts # DB container cleanup +│ ├── jest-e2e-setup.ts # Real DB test setup +│ ├── jest-mock-setup.ts # Mock test setup +│ └── test-database.helper.ts # DB helper utilities +└── utils/ # Test utilities + ├── mock-prisma.service.ts # Mock Prisma service + └── test-utils.ts # Helper functions +``` + +## Running Tests + +### Unit Tests +```bash +npm run test:unit +``` +Runs 37 unit tests. No external dependencies required. + +### Mock E2E Tests +```bash +npm run test:e2e:mock +# or +npm run test:e2e +``` +Runs 21 E2E tests with mocked database. No Docker or PostgreSQL required. + +### All Tests (Unit + Mock E2E) +```bash +npm run test:all +``` +Runs all 58 tests that don't require a real database. + +### Real Database E2E Tests + +#### Option 1: With Docker Desktop +```bash +# Ensure Docker Desktop is running +npm run test:e2e:db +``` +This will: +1. Start PostgreSQL container (port 5434) +2. Push Prisma schema +3. Run 32 E2E tests +4. Stop and cleanup container + +#### Option 2: With Existing PostgreSQL +```bash +# 1. Create a test database +psql -c "CREATE DATABASE rwa_backup_test;" + +# 2. Update DATABASE_URL in .env.test +# DATABASE_URL="postgresql://user:password@localhost:5432/rwa_backup_test" + +# 3. Setup database schema +npm run db:test:setup + +# 4. Run tests +npm run test:e2e:db:manual +``` + +## Test Commands + +| Command | Description | +|---------|-------------| +| `npm run test` | Run all Jest tests | +| `npm run test:unit` | Run unit tests only | +| `npm run test:e2e` | Run mock E2E tests | +| `npm run test:e2e:mock` | Run mock E2E tests | +| `npm run test:e2e:db` | Run real DB E2E tests (Docker) | +| `npm run test:e2e:db:manual` | Run real DB E2E tests (existing DB) | +| `npm run test:all` | Run unit + mock E2E tests | +| `npm run test:cov` | Run tests with coverage | +| `npm run db:test:up` | Start test DB container | +| `npm run db:test:down` | Stop test DB container | +| `npm run db:test:setup` | Setup existing DB for tests | + +## Test Environment Variables + +Located in `.env.test`: + +```env +DATABASE_URL="postgresql://postgres:testpassword@localhost:5434/rwa_backup_test" +APP_PORT=3003 +APP_ENV="test" +SERVICE_JWT_SECRET="test-super-secret-service-jwt-key-for-e2e-testing" +ALLOWED_SERVICES="identity-service,recovery-service" +BACKUP_ENCRYPTION_KEY="0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" +BACKUP_ENCRYPTION_KEY_ID="test-key-v1" +``` + +## Test Utilities + +### generateServiceToken(service, secret?) +Generates a JWT service token for testing authenticated endpoints. + +### createStoreSharePayload(overrides?) +Creates a valid store share request payload. + +### createRetrieveSharePayload(overrides?) +Creates a valid retrieve share request payload. + +### createRevokeSharePayload(overrides?) +Creates a valid revoke share request payload. + +### MockPrismaService +Full mock implementation of PrismaService for testing without a database. + +## Coverage Report + +Generate coverage report: +```bash +npm run test:cov +``` + +Coverage output in `./coverage/` directory. diff --git a/backend/services/backup-service/test/e2e/backup-share-mock.e2e-spec.ts b/backend/services/backup-service/test/e2e/backup-share-mock.e2e-spec.ts new file mode 100644 index 00000000..ace10454 --- /dev/null +++ b/backend/services/backup-service/test/e2e/backup-share-mock.e2e-spec.ts @@ -0,0 +1,433 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { INestApplication, ValidationPipe } from '@nestjs/common'; +import request from 'supertest'; +import { ConfigModule } from '@nestjs/config'; +import { ApiModule } from '../../src/api/api.module'; +import { ApplicationModule } from '../../src/application/application.module'; +import { InfrastructureModule } from '../../src/infrastructure/infrastructure.module'; +import { DomainModule } from '../../src/domain/domain.module'; +import { PrismaService } from '../../src/infrastructure/persistence/prisma/prisma.service'; +import { GlobalExceptionFilter } from '../../src/shared/filters/global-exception.filter'; +import { MockPrismaService } from '../utils/mock-prisma.service'; +import { + generateServiceToken, + generatePublicKey, + createStoreSharePayload, + createRetrieveSharePayload, + createRevokeSharePayload, + setupTestEnv, + TEST_SERVICE_JWT_SECRET, +} from '../utils/test-utils'; + +describe('BackupShare E2E (Mock Database)', () => { + let app: INestApplication; + let mockPrisma: MockPrismaService; + let serviceToken: string; + + beforeAll(async () => { + setupTestEnv(); + + mockPrisma = new MockPrismaService(); + + const moduleFixture: TestingModule = await Test.createTestingModule({ + imports: [ + ConfigModule.forRoot({ + isGlobal: true, + load: [ + () => ({ + app: { port: 3002, env: 'test' }, + security: { + serviceJwtSecret: TEST_SERVICE_JWT_SECRET, + allowedServices: ['identity-service', 'recovery-service'], + }, + encryption: { + key: process.env.BACKUP_ENCRYPTION_KEY, + keyId: process.env.BACKUP_ENCRYPTION_KEY_ID, + }, + rateLimit: { maxRetrievePerDay: 3 }, + }), + ], + }), + DomainModule, + InfrastructureModule, + ApplicationModule, + ApiModule, + ], + }) + .overrideProvider(PrismaService) + .useValue(mockPrisma) + .compile(); + + app = moduleFixture.createNestApplication(); + app.useGlobalPipes( + new ValidationPipe({ + whitelist: true, + forbidNonWhitelisted: true, + transform: true, + }), + ); + app.useGlobalFilters(new GlobalExceptionFilter()); + + await app.init(); + + serviceToken = generateServiceToken('identity-service'); + }); + + beforeEach(() => { + mockPrisma.reset(); + }); + + afterAll(async () => { + await app?.close(); + }); + + describe('Complete Workflow Tests', () => { + it('should complete full lifecycle: store -> retrieve -> revoke', async () => { + const publicKey = generatePublicKey('x'); + const storePayload = createStoreSharePayload({ + userId: '11111', + accountSequence: 1111, + publicKey, + }); + + // Step 1: Store + const storeResponse = await request(app.getHttpServer()) + .post('/backup-share/store') + .set('X-Service-Token', serviceToken) + .send(storePayload) + .expect(201); + + expect(storeResponse.body.success).toBe(true); + expect(storeResponse.body.shareId).toBeDefined(); + + // Step 2: Retrieve + const retrieveResponse = await request(app.getHttpServer()) + .post('/backup-share/retrieve') + .set('X-Service-Token', serviceToken) + .send(createRetrieveSharePayload({ userId: '11111', publicKey })) + .expect(200); + + expect(retrieveResponse.body.success).toBe(true); + expect(retrieveResponse.body.partyIndex).toBe(2); + + // Step 3: Revoke + const revokeResponse = await request(app.getHttpServer()) + .post('/backup-share/revoke') + .set('X-Service-Token', serviceToken) + .send(createRevokeSharePayload({ userId: '11111', publicKey, reason: 'ROTATION' })) + .expect(200); + + expect(revokeResponse.body.success).toBe(true); + + // Step 4: Verify cannot retrieve after revoke + await request(app.getHttpServer()) + .post('/backup-share/retrieve') + .set('X-Service-Token', serviceToken) + .send(createRetrieveSharePayload({ userId: '11111', publicKey })) + .expect(400); + }); + + it('should allow recovery-service to access shares', async () => { + const recoveryToken = generateServiceToken('recovery-service'); + const publicKey = generatePublicKey('r'); + + // Store with identity-service + await request(app.getHttpServer()) + .post('/backup-share/store') + .set('X-Service-Token', serviceToken) + .send(createStoreSharePayload({ userId: '22222', accountSequence: 2222, publicKey })) + .expect(201); + + // Retrieve with recovery-service + const response = await request(app.getHttpServer()) + .post('/backup-share/retrieve') + .set('X-Service-Token', recoveryToken) + .send(createRetrieveSharePayload({ userId: '22222', publicKey })) + .expect(200); + + expect(response.body.success).toBe(true); + }); + }); + + describe('Validation Tests', () => { + it('should reject store with missing userId', async () => { + const payload = { ...createStoreSharePayload(), userId: undefined }; + delete (payload as any).userId; + + await request(app.getHttpServer()) + .post('/backup-share/store') + .set('X-Service-Token', serviceToken) + .send(payload) + .expect(400); + }); + + it('should reject store with missing accountSequence', async () => { + const payload = { ...createStoreSharePayload(), accountSequence: undefined }; + delete (payload as any).accountSequence; + + await request(app.getHttpServer()) + .post('/backup-share/store') + .set('X-Service-Token', serviceToken) + .send(payload) + .expect(400); + }); + + it('should reject store with invalid public key length', async () => { + await request(app.getHttpServer()) + .post('/backup-share/store') + .set('X-Service-Token', serviceToken) + .send(createStoreSharePayload({ publicKey: 'too-short' })) + .expect(400); + }); + + it('should reject retrieve with missing recoveryToken', async () => { + const payload = { ...createRetrieveSharePayload(), recoveryToken: undefined }; + delete (payload as any).recoveryToken; + + await request(app.getHttpServer()) + .post('/backup-share/retrieve') + .set('X-Service-Token', serviceToken) + .send(payload) + .expect(400); + }); + + it('should reject revoke with invalid reason', async () => { + await request(app.getHttpServer()) + .post('/backup-share/revoke') + .set('X-Service-Token', serviceToken) + .send(createRevokeSharePayload({ reason: 'INVALID_REASON' })) + .expect(400); + }); + + it('should accept all valid revoke reasons', async () => { + const validReasons = ['ROTATION', 'ACCOUNT_CLOSED', 'SECURITY_BREACH', 'USER_REQUEST']; + + for (let i = 0; i < validReasons.length; i++) { + const publicKey = generatePublicKey(String(i)); + const userId = String(30000 + i); + + // Store first + await request(app.getHttpServer()) + .post('/backup-share/store') + .set('X-Service-Token', serviceToken) + .send(createStoreSharePayload({ userId, accountSequence: 30000 + i, publicKey })) + .expect(201); + + // Then revoke with valid reason + await request(app.getHttpServer()) + .post('/backup-share/revoke') + .set('X-Service-Token', serviceToken) + .send(createRevokeSharePayload({ userId, publicKey, reason: validReasons[i] })) + .expect(200); + } + }); + }); + + describe('Authentication Tests', () => { + it('should reject requests without token', async () => { + await request(app.getHttpServer()) + .post('/backup-share/store') + .send(createStoreSharePayload()) + .expect(401); + + await request(app.getHttpServer()) + .post('/backup-share/retrieve') + .send(createRetrieveSharePayload()) + .expect(401); + + await request(app.getHttpServer()) + .post('/backup-share/revoke') + .send(createRevokeSharePayload()) + .expect(401); + }); + + it('should reject requests with malformed token', async () => { + await request(app.getHttpServer()) + .post('/backup-share/store') + .set('X-Service-Token', 'not.a.valid.jwt') + .send(createStoreSharePayload()) + .expect(401); + }); + + it('should reject requests from unknown services', async () => { + const unknownToken = generateServiceToken('malicious-service'); + + await request(app.getHttpServer()) + .post('/backup-share/store') + .set('X-Service-Token', unknownToken) + .send(createStoreSharePayload()) + .expect(401); + }); + + it('should reject tokens signed with wrong secret', async () => { + const badToken = generateServiceToken('identity-service', 'wrong-secret'); + + await request(app.getHttpServer()) + .post('/backup-share/store') + .set('X-Service-Token', badToken) + .send(createStoreSharePayload()) + .expect(401); + }); + }); + + describe('Error Handling Tests', () => { + it('should return SHARE_NOT_FOUND for non-existent share', async () => { + const response = await request(app.getHttpServer()) + .post('/backup-share/retrieve') + .set('X-Service-Token', serviceToken) + .send(createRetrieveSharePayload({ userId: '99999999', publicKey: generatePublicKey('z') })) + .expect(404); + + expect(response.body.code).toBe('SHARE_NOT_FOUND'); + }); + + it('should return SHARE_ALREADY_EXISTS for duplicate store', async () => { + const publicKey = generatePublicKey('d'); + const payload = createStoreSharePayload({ userId: '44444', accountSequence: 4444, publicKey }); + + await request(app.getHttpServer()) + .post('/backup-share/store') + .set('X-Service-Token', serviceToken) + .send(payload) + .expect(201); + + const response = await request(app.getHttpServer()) + .post('/backup-share/store') + .set('X-Service-Token', serviceToken) + .send(payload) + .expect(409); + + expect(response.body.code).toBe('SHARE_ALREADY_EXISTS'); + }); + + it('should return SHARE_NOT_ACTIVE for revoked share retrieval', async () => { + const publicKey = generatePublicKey('e'); + + await request(app.getHttpServer()) + .post('/backup-share/store') + .set('X-Service-Token', serviceToken) + .send(createStoreSharePayload({ userId: '55555', accountSequence: 5555, publicKey })) + .expect(201); + + await request(app.getHttpServer()) + .post('/backup-share/revoke') + .set('X-Service-Token', serviceToken) + .send(createRevokeSharePayload({ userId: '55555', publicKey })) + .expect(200); + + const response = await request(app.getHttpServer()) + .post('/backup-share/retrieve') + .set('X-Service-Token', serviceToken) + .send(createRetrieveSharePayload({ userId: '55555', publicKey })) + .expect(400); + + expect(response.body.code).toBe('SHARE_NOT_ACTIVE'); + }); + }); + + describe('Optional Parameters Tests', () => { + it('should accept custom threshold and totalParties', async () => { + const response = await request(app.getHttpServer()) + .post('/backup-share/store') + .set('X-Service-Token', serviceToken) + .send(createStoreSharePayload({ + userId: '66666', + accountSequence: 6666, + publicKey: generatePublicKey('f'), + threshold: 3, + totalParties: 5, + })) + .expect(201); + + expect(response.body.success).toBe(true); + }); + + it('should accept deviceId in retrieve', async () => { + const publicKey = generatePublicKey('g'); + + await request(app.getHttpServer()) + .post('/backup-share/store') + .set('X-Service-Token', serviceToken) + .send(createStoreSharePayload({ userId: '77777', accountSequence: 7777, publicKey })) + .expect(201); + + const response = await request(app.getHttpServer()) + .post('/backup-share/retrieve') + .set('X-Service-Token', serviceToken) + .send(createRetrieveSharePayload({ userId: '77777', publicKey, deviceId: 'device-abc-123' })) + .expect(200); + + expect(response.body.success).toBe(true); + }); + }); + + describe('Response Structure Tests', () => { + it('should return correct store response structure', async () => { + const response = await request(app.getHttpServer()) + .post('/backup-share/store') + .set('X-Service-Token', serviceToken) + .send(createStoreSharePayload({ + userId: '80001', + accountSequence: 80001, + publicKey: generatePublicKey('s'), + })) + .expect(201); + + expect(response.body).toHaveProperty('success', true); + expect(response.body).toHaveProperty('shareId'); + expect(response.body).toHaveProperty('message', 'Backup share stored successfully'); + }); + + it('should return correct retrieve response structure', async () => { + const publicKey = generatePublicKey('t'); + await request(app.getHttpServer()) + .post('/backup-share/store') + .set('X-Service-Token', serviceToken) + .send(createStoreSharePayload({ userId: '80002', accountSequence: 80002, publicKey })) + .expect(201); + + const response = await request(app.getHttpServer()) + .post('/backup-share/retrieve') + .set('X-Service-Token', serviceToken) + .send(createRetrieveSharePayload({ userId: '80002', publicKey })) + .expect(200); + + expect(response.body).toHaveProperty('success', true); + expect(response.body).toHaveProperty('encryptedShareData'); + expect(response.body).toHaveProperty('partyIndex', 2); + expect(response.body).toHaveProperty('publicKey', publicKey); + }); + + it('should return correct revoke response structure', async () => { + const publicKey = generatePublicKey('u'); + await request(app.getHttpServer()) + .post('/backup-share/store') + .set('X-Service-Token', serviceToken) + .send(createStoreSharePayload({ userId: '80003', accountSequence: 80003, publicKey })) + .expect(201); + + const response = await request(app.getHttpServer()) + .post('/backup-share/revoke') + .set('X-Service-Token', serviceToken) + .send(createRevokeSharePayload({ userId: '80003', publicKey })) + .expect(200); + + expect(response.body).toHaveProperty('success', true); + expect(response.body).toHaveProperty('message', 'Backup share revoked successfully'); + }); + + it('should return correct error response structure', async () => { + const response = await request(app.getHttpServer()) + .post('/backup-share/retrieve') + .set('X-Service-Token', serviceToken) + .send(createRetrieveSharePayload({ userId: '99999', publicKey: generatePublicKey('v') })) + .expect(404); + + expect(response.body).toHaveProperty('success', false); + expect(response.body).toHaveProperty('error'); + expect(response.body).toHaveProperty('code'); + expect(response.body).toHaveProperty('timestamp'); + expect(response.body).toHaveProperty('path'); + }); + }); +}); diff --git a/backend/services/backup-service/test/e2e/backup-share.e2e-spec.ts b/backend/services/backup-service/test/e2e/backup-share.e2e-spec.ts new file mode 100644 index 00000000..e5adfb71 --- /dev/null +++ b/backend/services/backup-service/test/e2e/backup-share.e2e-spec.ts @@ -0,0 +1,517 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { INestApplication, ValidationPipe } from '@nestjs/common'; +import request from 'supertest'; +import { AppModule } from '../../src/app.module'; +import { PrismaService } from '../../src/infrastructure/persistence/prisma/prisma.service'; +import { + generateServiceToken, + generatePublicKey, + createStoreSharePayload, + createRetrieveSharePayload, + createRevokeSharePayload, + TEST_SERVICE_JWT_SECRET, +} from '../utils/test-utils'; + +describe('BackupShare E2E (Real Database)', () => { + let app: INestApplication; + let prisma: PrismaService; + let serviceToken: string; + + beforeAll(async () => { + // Environment variables are loaded by jest-e2e-setup.ts + process.env.SERVICE_JWT_SECRET = TEST_SERVICE_JWT_SECRET; + + const moduleFixture: TestingModule = await Test.createTestingModule({ + imports: [AppModule], + }).compile(); + + app = moduleFixture.createNestApplication(); + app.useGlobalPipes( + new ValidationPipe({ + whitelist: true, + forbidNonWhitelisted: true, + transform: true, + }), + ); + + await app.init(); + + prisma = app.get(PrismaService); + serviceToken = generateServiceToken('identity-service'); + }); + + beforeEach(async () => { + // Clean database before each test + if (prisma) { + await prisma.shareAccessLog.deleteMany(); + await prisma.backupShare.deleteMany(); + } + }); + + afterAll(async () => { + await app?.close(); + }); + + describe('Health Check', () => { + it('GET /health should return ok', () => { + return request(app.getHttpServer()) + .get('/health') + .expect(200) + .expect((res) => { + expect(res.body.status).toBe('ok'); + expect(res.body.service).toBe('backup-service'); + }); + }); + + it('GET /health/live should return alive', () => { + return request(app.getHttpServer()) + .get('/health/live') + .expect(200) + .expect((res) => { + expect(res.body.status).toBe('alive'); + }); + }); + }); + + describe('POST /backup-share/store', () => { + it('should store backup share successfully', async () => { + const payload = createStoreSharePayload({ + userId: '10001', + accountSequence: 10001, + publicKey: generatePublicKey('a'), + }); + + const response = await request(app.getHttpServer()) + .post('/backup-share/store') + .set('X-Service-Token', serviceToken) + .send(payload) + .expect(201); + + expect(response.body.success).toBe(true); + expect(response.body.shareId).toBeDefined(); + expect(response.body.message).toBe('Backup share stored successfully'); + + // Verify data persisted to database + const share = await prisma.backupShare.findUnique({ + where: { userId: BigInt(payload.userId) }, + }); + expect(share).not.toBeNull(); + expect(share?.publicKey).toBe(payload.publicKey); + expect(share?.status).toBe('ACTIVE'); + }); + + it('should reject duplicate share for same user', async () => { + const payload = createStoreSharePayload({ + userId: '10002', + accountSequence: 10002, + publicKey: generatePublicKey('b'), + }); + + // First store + await request(app.getHttpServer()) + .post('/backup-share/store') + .set('X-Service-Token', serviceToken) + .send(payload) + .expect(201); + + // Duplicate attempt + const response = await request(app.getHttpServer()) + .post('/backup-share/store') + .set('X-Service-Token', serviceToken) + .send(payload) + .expect(409); + + expect(response.body.success).toBe(false); + expect(response.body.code).toBe('SHARE_ALREADY_EXISTS'); + }); + + it('should reject request without service token', () => { + return request(app.getHttpServer()) + .post('/backup-share/store') + .send(createStoreSharePayload()) + .expect(401); + }); + + it('should reject request with invalid service token', () => { + return request(app.getHttpServer()) + .post('/backup-share/store') + .set('X-Service-Token', 'invalid-token') + .send(createStoreSharePayload()) + .expect(401); + }); + + it('should reject request from unauthorized service', () => { + const unauthorizedToken = generateServiceToken('unknown-service'); + + return request(app.getHttpServer()) + .post('/backup-share/store') + .set('X-Service-Token', unauthorizedToken) + .send(createStoreSharePayload()) + .expect(401); + }); + + it('should validate required fields', async () => { + const response = await request(app.getHttpServer()) + .post('/backup-share/store') + .set('X-Service-Token', serviceToken) + .send({}) + .expect(400); + + expect(response.body.success).toBe(false); + }); + + it('should validate public key length', async () => { + const response = await request(app.getHttpServer()) + .post('/backup-share/store') + .set('X-Service-Token', serviceToken) + .send({ + ...createStoreSharePayload(), + publicKey: 'short', + }) + .expect(400); + + expect(response.body.success).toBe(false); + }); + }); + + describe('POST /backup-share/retrieve', () => { + const storePayload = { + userId: '20001', + accountSequence: 20001, + publicKey: generatePublicKey('c'), + encryptedShareData: 'test-encrypted-data', + }; + + beforeEach(async () => { + // Store a share first + await request(app.getHttpServer()) + .post('/backup-share/store') + .set('X-Service-Token', serviceToken) + .send(storePayload); + }); + + it('should retrieve backup share successfully', async () => { + const response = await request(app.getHttpServer()) + .post('/backup-share/retrieve') + .set('X-Service-Token', serviceToken) + .send(createRetrieveSharePayload({ + userId: storePayload.userId, + publicKey: storePayload.publicKey, + })) + .expect(200); + + expect(response.body.success).toBe(true); + expect(response.body.encryptedShareData).toBeDefined(); + expect(response.body.partyIndex).toBe(2); + expect(response.body.publicKey).toBe(storePayload.publicKey); + + // Verify access log created for RETRIEVE action + const accessLogs = await prisma.shareAccessLog.findMany({ + where: { + userId: BigInt(storePayload.userId), + action: 'RETRIEVE', + }, + }); + expect(accessLogs.length).toBeGreaterThan(0); + expect(accessLogs[0].action).toBe('RETRIEVE'); + }); + + it('should return 404 for non-existent share', async () => { + const response = await request(app.getHttpServer()) + .post('/backup-share/retrieve') + .set('X-Service-Token', serviceToken) + .send(createRetrieveSharePayload({ + userId: '99999', + publicKey: generatePublicKey('z'), + })) + .expect(404); + + expect(response.body.success).toBe(false); + expect(response.body.code).toBe('SHARE_NOT_FOUND'); + }); + + it('should increment access count on retrieve', async () => { + // First retrieve + await request(app.getHttpServer()) + .post('/backup-share/retrieve') + .set('X-Service-Token', serviceToken) + .send(createRetrieveSharePayload({ + userId: storePayload.userId, + publicKey: storePayload.publicKey, + })) + .expect(200); + + // Second retrieve + await request(app.getHttpServer()) + .post('/backup-share/retrieve') + .set('X-Service-Token', serviceToken) + .send(createRetrieveSharePayload({ + userId: storePayload.userId, + publicKey: storePayload.publicKey, + })) + .expect(200); + + // Verify access count + const share = await prisma.backupShare.findUnique({ + where: { userId: BigInt(storePayload.userId) }, + }); + expect(share?.accessCount).toBe(2); + }); + }); + + describe('POST /backup-share/revoke', () => { + const storePayload = { + userId: '30001', + accountSequence: 30001, + publicKey: generatePublicKey('e'), + encryptedShareData: 'test-data', + }; + + beforeEach(async () => { + await request(app.getHttpServer()) + .post('/backup-share/store') + .set('X-Service-Token', serviceToken) + .send(storePayload); + }); + + it('should revoke backup share successfully', async () => { + const response = await request(app.getHttpServer()) + .post('/backup-share/revoke') + .set('X-Service-Token', serviceToken) + .send(createRevokeSharePayload({ + userId: storePayload.userId, + publicKey: storePayload.publicKey, + reason: 'ROTATION', + })) + .expect(200); + + expect(response.body.success).toBe(true); + expect(response.body.message).toBe('Backup share revoked successfully'); + + // Verify share status in database + const share = await prisma.backupShare.findUnique({ + where: { userId: BigInt(storePayload.userId) }, + }); + expect(share?.status).toBe('REVOKED'); + }); + + it('should not allow retrieval of revoked share', async () => { + // Revoke first + await request(app.getHttpServer()) + .post('/backup-share/revoke') + .set('X-Service-Token', serviceToken) + .send(createRevokeSharePayload({ + userId: storePayload.userId, + publicKey: storePayload.publicKey, + reason: 'SECURITY_BREACH', + })) + .expect(200); + + // Try to retrieve + const response = await request(app.getHttpServer()) + .post('/backup-share/retrieve') + .set('X-Service-Token', serviceToken) + .send(createRetrieveSharePayload({ + userId: storePayload.userId, + publicKey: storePayload.publicKey, + })) + .expect(400); + + expect(response.body.code).toBe('SHARE_NOT_ACTIVE'); + }); + + it('should validate reason field', async () => { + const response = await request(app.getHttpServer()) + .post('/backup-share/revoke') + .set('X-Service-Token', serviceToken) + .send({ + userId: storePayload.userId, + publicKey: storePayload.publicKey, + reason: 'INVALID_REASON', + }) + .expect(400); + + expect(response.body.success).toBe(false); + }); + + it('should accept all valid revoke reasons', async () => { + const validReasons = ['ROTATION', 'ACCOUNT_CLOSED', 'SECURITY_BREACH', 'USER_REQUEST']; + + for (let i = 0; i < validReasons.length; i++) { + const payload = { + userId: String(40000 + i), + accountSequence: 40000 + i, + publicKey: generatePublicKey(String(i)), + encryptedShareData: 'test-data', + }; + + // Store share + await request(app.getHttpServer()) + .post('/backup-share/store') + .set('X-Service-Token', serviceToken) + .send(payload) + .expect(201); + + // Revoke with valid reason + const response = await request(app.getHttpServer()) + .post('/backup-share/revoke') + .set('X-Service-Token', serviceToken) + .send({ + userId: payload.userId, + publicKey: payload.publicKey, + reason: validReasons[i], + }) + .expect(200); + + expect(response.body.success).toBe(true); + } + }); + }); + + describe('Complete Workflow Tests', () => { + it('should complete full lifecycle: store -> retrieve -> revoke', async () => { + const publicKey = generatePublicKey('x'); + const storePayload = createStoreSharePayload({ + userId: '50001', + accountSequence: 50001, + publicKey, + }); + + // Step 1: Store + const storeResponse = await request(app.getHttpServer()) + .post('/backup-share/store') + .set('X-Service-Token', serviceToken) + .send(storePayload) + .expect(201); + + expect(storeResponse.body.success).toBe(true); + expect(storeResponse.body.shareId).toBeDefined(); + + // Step 2: Retrieve + const retrieveResponse = await request(app.getHttpServer()) + .post('/backup-share/retrieve') + .set('X-Service-Token', serviceToken) + .send(createRetrieveSharePayload({ userId: '50001', publicKey })) + .expect(200); + + expect(retrieveResponse.body.success).toBe(true); + expect(retrieveResponse.body.partyIndex).toBe(2); + + // Step 3: Revoke + const revokeResponse = await request(app.getHttpServer()) + .post('/backup-share/revoke') + .set('X-Service-Token', serviceToken) + .send(createRevokeSharePayload({ userId: '50001', publicKey, reason: 'ROTATION' })) + .expect(200); + + expect(revokeResponse.body.success).toBe(true); + + // Step 4: Verify cannot retrieve after revoke + await request(app.getHttpServer()) + .post('/backup-share/retrieve') + .set('X-Service-Token', serviceToken) + .send(createRetrieveSharePayload({ userId: '50001', publicKey })) + .expect(400); + }); + + it('should allow recovery-service to access shares', async () => { + const recoveryToken = generateServiceToken('recovery-service'); + const publicKey = generatePublicKey('r'); + + // Store with identity-service + await request(app.getHttpServer()) + .post('/backup-share/store') + .set('X-Service-Token', serviceToken) + .send(createStoreSharePayload({ userId: '50002', accountSequence: 50002, publicKey })) + .expect(201); + + // Retrieve with recovery-service + const response = await request(app.getHttpServer()) + .post('/backup-share/retrieve') + .set('X-Service-Token', recoveryToken) + .send(createRetrieveSharePayload({ userId: '50002', publicKey })) + .expect(200); + + expect(response.body.success).toBe(true); + }); + }); + + describe('Data Persistence Tests', () => { + it('should persist encrypted data correctly', async () => { + const encryptedData = 'base64-encoded-encrypted-share-data-12345'; + const payload = createStoreSharePayload({ + userId: '60001', + accountSequence: 60001, + publicKey: generatePublicKey('p'), + encryptedShareData: encryptedData, + }); + + await request(app.getHttpServer()) + .post('/backup-share/store') + .set('X-Service-Token', serviceToken) + .send(payload) + .expect(201); + + // Verify data in database (it should be double-encrypted) + const share = await prisma.backupShare.findUnique({ + where: { userId: BigInt(payload.userId) }, + }); + + expect(share).not.toBeNull(); + // The stored data should be different from input (encrypted) + expect(share?.encryptedShareData).not.toBe(encryptedData); + // But retrieving should give us back the original + const retrieveResponse = await request(app.getHttpServer()) + .post('/backup-share/retrieve') + .set('X-Service-Token', serviceToken) + .send(createRetrieveSharePayload({ + userId: payload.userId, + publicKey: payload.publicKey, + })) + .expect(200); + + expect(retrieveResponse.body.encryptedShareData).toBe(encryptedData); + }); + + it('should create audit logs for all operations', async () => { + const publicKey = generatePublicKey('q'); + const userId = '60003'; + + // Store + await request(app.getHttpServer()) + .post('/backup-share/store') + .set('X-Service-Token', serviceToken) + .send(createStoreSharePayload({ + userId, + accountSequence: 60003, + publicKey, + })) + .expect(201); + + // Retrieve + await request(app.getHttpServer()) + .post('/backup-share/retrieve') + .set('X-Service-Token', serviceToken) + .send(createRetrieveSharePayload({ userId, publicKey })) + .expect(200); + + // Revoke + await request(app.getHttpServer()) + .post('/backup-share/revoke') + .set('X-Service-Token', serviceToken) + .send(createRevokeSharePayload({ userId, publicKey })) + .expect(200); + + // Check audit logs + const logs = await prisma.shareAccessLog.findMany({ + where: { userId: BigInt(userId) }, + orderBy: { createdAt: 'asc' }, + }); + + expect(logs.length).toBe(3); + expect(logs[0].action).toBe('STORE'); + expect(logs[1].action).toBe('RETRIEVE'); + expect(logs[2].action).toBe('REVOKE'); + }); + }); +}); diff --git a/backend/services/backup-service/test/integration/audit-log-repository.integration.spec.ts b/backend/services/backup-service/test/integration/audit-log-repository.integration.spec.ts new file mode 100644 index 00000000..5b4116a9 --- /dev/null +++ b/backend/services/backup-service/test/integration/audit-log-repository.integration.spec.ts @@ -0,0 +1,204 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { AuditLogRepository } from '../../src/infrastructure/persistence/repositories/audit-log.repository'; +import { PrismaService } from '../../src/infrastructure/persistence/prisma/prisma.service'; +import { MockPrismaService } from '../utils/mock-prisma.service'; + +describe('AuditLogRepository Integration', () => { + let repository: AuditLogRepository; + let mockPrisma: MockPrismaService; + + beforeEach(async () => { + mockPrisma = new MockPrismaService(); + + const module: TestingModule = await Test.createTestingModule({ + providers: [ + AuditLogRepository, + { provide: PrismaService, useValue: mockPrisma }, + ], + }).compile(); + + repository = module.get(AuditLogRepository); + }); + + afterEach(() => { + mockPrisma.reset(); + }); + + describe('log', () => { + it('should create an audit log entry for STORE action', async () => { + await repository.log({ + shareId: BigInt(1), + userId: BigInt(12345), + action: 'STORE', + sourceService: 'identity-service', + sourceIp: '192.168.1.1', + success: true, + }); + + expect(mockPrisma.shareAccessLog.create).toHaveBeenCalledWith({ + data: expect.objectContaining({ + shareId: BigInt(1), + userId: BigInt(12345), + action: 'STORE', + sourceService: 'identity-service', + sourceIp: '192.168.1.1', + success: true, + }), + }); + }); + + it('should create an audit log entry for RETRIEVE action', async () => { + await repository.log({ + shareId: BigInt(2), + userId: BigInt(67890), + action: 'RETRIEVE', + sourceService: 'recovery-service', + sourceIp: '10.0.0.1', + success: true, + }); + + expect(mockPrisma.shareAccessLog.create).toHaveBeenCalledWith({ + data: expect.objectContaining({ + action: 'RETRIEVE', + sourceService: 'recovery-service', + }), + }); + }); + + it('should create an audit log entry for REVOKE action', async () => { + await repository.log({ + shareId: BigInt(3), + userId: BigInt(11111), + action: 'REVOKE', + sourceService: 'identity-service', + sourceIp: '172.16.0.1', + success: true, + }); + + expect(mockPrisma.shareAccessLog.create).toHaveBeenCalledWith({ + data: expect.objectContaining({ + action: 'REVOKE', + }), + }); + }); + + it('should log failed operations with error message', async () => { + await repository.log({ + shareId: BigInt(0), + userId: BigInt(22222), + action: 'RETRIEVE', + sourceService: 'identity-service', + sourceIp: '192.168.1.100', + success: false, + errorMessage: 'Share not found', + }); + + expect(mockPrisma.shareAccessLog.create).toHaveBeenCalledWith({ + data: expect.objectContaining({ + success: false, + errorMessage: 'Share not found', + }), + }); + }); + + it('should not throw when database error occurs', async () => { + mockPrisma.shareAccessLog.create.mockRejectedValueOnce(new Error('DB Error')); + + await expect( + repository.log({ + shareId: BigInt(1), + userId: BigInt(12345), + action: 'STORE', + sourceService: 'identity-service', + sourceIp: '127.0.0.1', + success: true, + }), + ).resolves.not.toThrow(); + }); + }); + + describe('findByShareId', () => { + it('should find logs by share ID', async () => { + const shareId = BigInt(100); + + // Create some logs + await repository.log({ + shareId, + userId: BigInt(12345), + action: 'STORE', + sourceService: 'identity-service', + sourceIp: '127.0.0.1', + success: true, + }); + await repository.log({ + shareId, + userId: BigInt(12345), + action: 'RETRIEVE', + sourceService: 'identity-service', + sourceIp: '127.0.0.1', + success: true, + }); + + await repository.findByShareId(shareId); + + expect(mockPrisma.shareAccessLog.findMany).toHaveBeenCalledWith({ + where: { shareId }, + orderBy: { createdAt: 'desc' }, + take: 100, + }); + }); + + it('should respect limit parameter', async () => { + await repository.findByShareId(BigInt(1), 50); + + expect(mockPrisma.shareAccessLog.findMany).toHaveBeenCalledWith( + expect.objectContaining({ + take: 50, + }), + ); + }); + }); + + describe('findByUserId', () => { + it('should find logs by user ID', async () => { + const userId = BigInt(55555); + + await repository.findByUserId(userId); + + expect(mockPrisma.shareAccessLog.findMany).toHaveBeenCalledWith({ + where: { userId }, + orderBy: { createdAt: 'desc' }, + take: 100, + }); + }); + }); + + describe('countRetrievesByUserToday', () => { + it('should count today\'s retrieves for a user', async () => { + const userId = BigInt(33333); + + await repository.countRetrievesByUserToday(userId); + + expect(mockPrisma.shareAccessLog.count).toHaveBeenCalledWith({ + where: { + userId, + action: 'RETRIEVE', + createdAt: expect.objectContaining({ + gte: expect.any(Date), + }), + }, + }); + }); + + it('should return correct count', async () => { + const userId = BigInt(44444); + + // Simulate 2 retrieves today + mockPrisma.shareAccessLog.count.mockResolvedValueOnce(2); + + const count = await repository.countRetrievesByUserToday(userId); + + expect(count).toBe(2); + }); + }); +}); diff --git a/backend/services/backup-service/test/integration/backup-share-repository.integration.spec.ts b/backend/services/backup-service/test/integration/backup-share-repository.integration.spec.ts new file mode 100644 index 00000000..a6e077b0 --- /dev/null +++ b/backend/services/backup-service/test/integration/backup-share-repository.integration.spec.ts @@ -0,0 +1,252 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { BackupShareRepositoryImpl } from '../../src/infrastructure/persistence/repositories/backup-share.repository.impl'; +import { PrismaService } from '../../src/infrastructure/persistence/prisma/prisma.service'; +import { MockPrismaService } from '../utils/mock-prisma.service'; +import { BackupShare, BackupShareStatus } from '../../src/domain'; + +describe('BackupShareRepository Integration', () => { + let repository: BackupShareRepositoryImpl; + let mockPrisma: MockPrismaService; + + beforeEach(async () => { + mockPrisma = new MockPrismaService(); + + const module: TestingModule = await Test.createTestingModule({ + providers: [ + BackupShareRepositoryImpl, + { provide: PrismaService, useValue: mockPrisma }, + ], + }).compile(); + + repository = module.get(BackupShareRepositoryImpl); + }); + + afterEach(() => { + mockPrisma.reset(); + }); + + describe('save', () => { + it('should create a new backup share', async () => { + const share = BackupShare.create({ + userId: BigInt(12345), + accountSequence: BigInt(1001), + publicKey: '02' + 'a'.repeat(64), + encryptedShareData: 'encrypted-data', + encryptionKeyId: 'key-v1', + }); + + const saved = await repository.save(share); + + expect(saved.shareId).toBeDefined(); + expect(saved.userId).toBe(BigInt(12345)); + expect(saved.publicKey).toBe('02' + 'a'.repeat(64)); + expect(mockPrisma.backupShare.create).toHaveBeenCalled(); + }); + + it('should update an existing backup share', async () => { + // First create + const share = BackupShare.create({ + userId: BigInt(12345), + accountSequence: BigInt(1001), + publicKey: '02' + 'a'.repeat(64), + encryptedShareData: 'encrypted-data', + encryptionKeyId: 'key-v1', + }); + + const saved = await repository.save(share); + + // Modify and update + saved.recordAccess(); + const updated = await repository.save(saved); + + expect(updated.accessCount).toBe(1); + expect(mockPrisma.backupShare.update).toHaveBeenCalled(); + }); + + it('should preserve all fields when saving', async () => { + const share = BackupShare.create({ + userId: BigInt(99999), + accountSequence: BigInt(9999), + publicKey: '02' + 'b'.repeat(64), + encryptedShareData: 'test-encrypted-data', + encryptionKeyId: 'key-v2', + threshold: 3, + totalParties: 5, + }); + + const saved = await repository.save(share); + + expect(saved.threshold).toBe(3); + expect(saved.totalParties).toBe(5); + expect(saved.partyIndex).toBe(2); + expect(saved.status).toBe(BackupShareStatus.ACTIVE); + }); + }); + + describe('findByUserId', () => { + it('should find share by user ID', async () => { + const share = BackupShare.create({ + userId: BigInt(55555), + accountSequence: BigInt(5555), + publicKey: '02' + 'c'.repeat(64), + encryptedShareData: 'data', + encryptionKeyId: 'key-v1', + }); + await repository.save(share); + + const found = await repository.findByUserId(BigInt(55555)); + + expect(found).toBeDefined(); + expect(found?.userId).toBe(BigInt(55555)); + }); + + it('should return null for non-existent user', async () => { + const found = await repository.findByUserId(BigInt(99999999)); + + expect(found).toBeNull(); + }); + }); + + describe('findByPublicKey', () => { + it('should find share by public key', async () => { + const publicKey = '02' + 'd'.repeat(64); + const share = BackupShare.create({ + userId: BigInt(66666), + accountSequence: BigInt(6666), + publicKey, + encryptedShareData: 'data', + encryptionKeyId: 'key-v1', + }); + await repository.save(share); + + const found = await repository.findByPublicKey(publicKey); + + expect(found).toBeDefined(); + expect(found?.publicKey).toBe(publicKey); + }); + }); + + describe('findByUserIdAndPublicKey', () => { + it('should find share by user ID and public key combination', async () => { + const userId = BigInt(77777); + const publicKey = '02' + 'e'.repeat(64); + const share = BackupShare.create({ + userId, + accountSequence: BigInt(7777), + publicKey, + encryptedShareData: 'data', + encryptionKeyId: 'key-v1', + }); + await repository.save(share); + + const found = await repository.findByUserIdAndPublicKey(userId, publicKey); + + expect(found).toBeDefined(); + expect(found?.userId).toBe(userId); + expect(found?.publicKey).toBe(publicKey); + }); + + it('should return null when user ID matches but public key does not', async () => { + const share = BackupShare.create({ + userId: BigInt(88888), + accountSequence: BigInt(8888), + publicKey: '02' + 'f'.repeat(64), + encryptedShareData: 'data', + encryptionKeyId: 'key-v1', + }); + await repository.save(share); + + const found = await repository.findByUserIdAndPublicKey( + BigInt(88888), + '02' + 'g'.repeat(64), // Different public key + ); + + expect(found).toBeNull(); + }); + }); + + describe('findByAccountSequence', () => { + it('should find share by account sequence', async () => { + const accountSequence = BigInt(123456); + const share = BackupShare.create({ + userId: BigInt(11111), + accountSequence, + publicKey: '02' + 'h'.repeat(64), + encryptedShareData: 'data', + encryptionKeyId: 'key-v1', + }); + await repository.save(share); + + const found = await repository.findByAccountSequence(accountSequence); + + expect(found).toBeDefined(); + expect(found?.accountSequence).toBe(accountSequence); + }); + }); + + describe('delete', () => { + it('should delete a backup share', async () => { + const share = BackupShare.create({ + userId: BigInt(22222), + accountSequence: BigInt(2222), + publicKey: '02' + 'i'.repeat(64), + encryptedShareData: 'data', + encryptionKeyId: 'key-v1', + }); + const saved = await repository.save(share); + + await repository.delete(saved.shareId!); + + expect(mockPrisma.backupShare.delete).toHaveBeenCalledWith({ + where: { shareId: saved.shareId }, + }); + }); + }); + + describe('entity state transitions', () => { + it('should persist revoked status', async () => { + const share = BackupShare.create({ + userId: BigInt(33333), + accountSequence: BigInt(3333), + publicKey: '02' + 'j'.repeat(64), + encryptedShareData: 'data', + encryptionKeyId: 'key-v1', + }); + const saved = await repository.save(share); + + saved.revoke('SECURITY_BREACH'); + await repository.save(saved); + + expect(mockPrisma.backupShare.update).toHaveBeenCalledWith( + expect.objectContaining({ + data: expect.objectContaining({ + status: 'REVOKED', + }), + }), + ); + }); + + it('should persist access count updates', async () => { + const share = BackupShare.create({ + userId: BigInt(44444), + accountSequence: BigInt(4444), + publicKey: '02' + 'k'.repeat(64), + encryptedShareData: 'data', + encryptionKeyId: 'key-v1', + }); + const saved = await repository.save(share); + + saved.recordAccess(); + saved.recordAccess(); + await repository.save(saved); + + expect(mockPrisma.backupShare.update).toHaveBeenCalledWith( + expect.objectContaining({ + data: expect.objectContaining({ + accessCount: 2, + }), + }), + ); + }); + }); +}); diff --git a/backend/services/backup-service/test/jest-e2e-db.json b/backend/services/backup-service/test/jest-e2e-db.json new file mode 100644 index 00000000..92bd408c --- /dev/null +++ b/backend/services/backup-service/test/jest-e2e-db.json @@ -0,0 +1,17 @@ +{ + "moduleFileExtensions": ["js", "json", "ts"], + "rootDir": ".", + "testEnvironment": "node", + "testRegex": "backup-share.e2e-spec.ts$", + "transform": { + "^.+\\.(t|j)s$": "ts-jest" + }, + "moduleNameMapper": { + "^src/(.*)$": "/../src/$1" + }, + "setupFilesAfterEnv": ["/setup/jest-e2e-setup.ts"], + "globalSetup": "/setup/global-setup.ts", + "globalTeardown": "/setup/global-teardown.ts", + "testTimeout": 60000, + "verbose": true +} diff --git a/backend/services/backup-service/test/jest-e2e-mock.json b/backend/services/backup-service/test/jest-e2e-mock.json new file mode 100644 index 00000000..e3eae6e3 --- /dev/null +++ b/backend/services/backup-service/test/jest-e2e-mock.json @@ -0,0 +1,15 @@ +{ + "moduleFileExtensions": ["js", "json", "ts"], + "rootDir": ".", + "testEnvironment": "node", + "testRegex": "mock.e2e-spec.ts$", + "transform": { + "^.+\\.(t|j)s$": "ts-jest" + }, + "moduleNameMapper": { + "^src/(.*)$": "/../src/$1" + }, + "setupFilesAfterEnv": ["/setup/jest-mock-setup.ts"], + "testTimeout": 30000, + "verbose": true +} diff --git a/backend/services/backup-service/test/jest-e2e.json b/backend/services/backup-service/test/jest-e2e.json new file mode 100644 index 00000000..376441c5 --- /dev/null +++ b/backend/services/backup-service/test/jest-e2e.json @@ -0,0 +1,17 @@ +{ + "moduleFileExtensions": ["js", "json", "ts"], + "rootDir": ".", + "testEnvironment": "node", + "testRegex": ".e2e-spec.ts$", + "transform": { + "^.+\\.(t|j)s$": "ts-jest" + }, + "moduleNameMapper": { + "^src/(.*)$": "/../src/$1" + }, + "setupFilesAfterEnv": ["/setup/jest-e2e-setup.ts"], + "globalSetup": "/setup/global-setup.ts", + "globalTeardown": "/setup/global-teardown.ts", + "testTimeout": 30000, + "verbose": true +} diff --git a/backend/services/backup-service/test/setup/global-setup.ts b/backend/services/backup-service/test/setup/global-setup.ts new file mode 100644 index 00000000..d4b3484d --- /dev/null +++ b/backend/services/backup-service/test/setup/global-setup.ts @@ -0,0 +1,97 @@ +import { execSync } from 'child_process'; +import * as path from 'path'; +import * as dotenv from 'dotenv'; + +// Load test environment +dotenv.config({ path: path.resolve(__dirname, '../../.env.test') }); + +export default async function globalSetup() { + console.log('\n🚀 Starting E2E test environment setup...'); + + try { + const useDocker = process.env.USE_DOCKER !== 'false'; + + if (useDocker) { + // Try to use Docker + const dockerAvailable = checkDockerAvailable(); + + if (dockerAvailable) { + // Start test database container + console.log('📦 Starting test database container...'); + execSync('docker-compose -f docker-compose.test.yml up -d', { + cwd: path.resolve(__dirname, '../..'), + stdio: 'inherit', + }); + } else { + console.log('⚠️ Docker not available. Assuming database is already running...'); + console.log(' Make sure PostgreSQL is running on port 5434'); + console.log(' Or set USE_DOCKER=false and configure DATABASE_URL'); + } + } else { + console.log('📌 Docker disabled. Using existing database...'); + } + + // Wait for database to be ready + console.log('⏳ Waiting for database to be ready...'); + await waitForDatabase(); + + // Push Prisma schema to database + console.log('🔄 Pushing Prisma schema to database...'); + execSync('npx prisma db push --force-reset --accept-data-loss', { + cwd: path.resolve(__dirname, '../..'), + stdio: 'inherit', + env: { + ...process.env, + DATABASE_URL: process.env.DATABASE_URL, + }, + }); + + // Generate Prisma client + console.log('🔧 Generating Prisma client...'); + execSync('npx prisma generate', { + cwd: path.resolve(__dirname, '../..'), + stdio: 'inherit', + }); + + console.log('✅ E2E test environment setup complete!\n'); + } catch (error) { + console.error('❌ Failed to setup E2E test environment:', error); + throw error; + } +} + +function checkDockerAvailable(): boolean { + try { + execSync('docker info', { stdio: 'ignore' }); + return true; + } catch { + return false; + } +} + +async function waitForDatabase(maxAttempts = 30, delayMs = 1000): Promise { + const { Client } = await import('pg'); + + for (let attempt = 1; attempt <= maxAttempts; attempt++) { + const client = new Client({ + connectionString: process.env.DATABASE_URL, + }); + + try { + await client.connect(); + await client.query('SELECT 1'); + await client.end(); + console.log(` Database ready after ${attempt} attempt(s)`); + return; + } catch (error) { + await client.end().catch(() => {}); + if (attempt === maxAttempts) { + throw new Error(`Database not ready after ${maxAttempts} attempts. Is PostgreSQL running on port 5434?`); + } + if (attempt % 5 === 0) { + console.log(` Still waiting for database... (attempt ${attempt}/${maxAttempts})`); + } + await new Promise((resolve) => setTimeout(resolve, delayMs)); + } + } +} diff --git a/backend/services/backup-service/test/setup/global-teardown.ts b/backend/services/backup-service/test/setup/global-teardown.ts new file mode 100644 index 00000000..fbdff24a --- /dev/null +++ b/backend/services/backup-service/test/setup/global-teardown.ts @@ -0,0 +1,29 @@ +import { execSync } from 'child_process'; +import * as path from 'path'; + +export default async function globalTeardown() { + console.log('\n🧹 Cleaning up E2E test environment...'); + + const useDocker = process.env.USE_DOCKER !== 'false'; + + if (useDocker) { + try { + // Check if Docker is available + execSync('docker info', { stdio: 'ignore' }); + + // Stop and remove test database container + console.log('📦 Stopping test database container...'); + execSync('docker-compose -f docker-compose.test.yml down -v', { + cwd: path.resolve(__dirname, '../..'), + stdio: 'inherit', + }); + + console.log('✅ E2E test environment cleanup complete!\n'); + } catch (error) { + console.log('⚠️ Docker not available or container already stopped'); + } + } else { + console.log('📌 Docker disabled. Skipping container cleanup...'); + console.log('✅ E2E test environment cleanup complete!\n'); + } +} diff --git a/backend/services/backup-service/test/setup/jest-e2e-setup.ts b/backend/services/backup-service/test/setup/jest-e2e-setup.ts new file mode 100644 index 00000000..101bad98 --- /dev/null +++ b/backend/services/backup-service/test/setup/jest-e2e-setup.ts @@ -0,0 +1,36 @@ +import * as path from 'path'; +import * as dotenv from 'dotenv'; + +// Load test environment variables before any tests run +dotenv.config({ path: path.resolve(__dirname, '../../.env.test') }); + +// Set test environment +process.env.NODE_ENV = 'test'; +process.env.APP_ENV = 'test'; + +// Increase timeout for E2E tests +jest.setTimeout(30000); + +// Add custom matchers if needed +expect.extend({ + toBeValidUUID(received: string) { + const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; + const pass = uuidRegex.test(received); + return { + pass, + message: () => + pass + ? `expected ${received} not to be a valid UUID` + : `expected ${received} to be a valid UUID`, + }; + }, +}); + +// Extend Jest types +declare global { + namespace jest { + interface Matchers { + toBeValidUUID(): R; + } + } +} diff --git a/backend/services/backup-service/test/setup/jest-mock-setup.ts b/backend/services/backup-service/test/setup/jest-mock-setup.ts new file mode 100644 index 00000000..2824655d --- /dev/null +++ b/backend/services/backup-service/test/setup/jest-mock-setup.ts @@ -0,0 +1,10 @@ +// Mock E2E test setup - no database needed +process.env.NODE_ENV = 'test'; +process.env.APP_ENV = 'test'; +process.env.SERVICE_JWT_SECRET = 'test-super-secret-service-jwt-key-for-e2e-testing'; +process.env.BACKUP_ENCRYPTION_KEY = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef'; +process.env.BACKUP_ENCRYPTION_KEY_ID = 'test-key-v1'; +process.env.ALLOWED_SERVICES = 'identity-service,recovery-service'; + +// Increase timeout for E2E tests +jest.setTimeout(30000); diff --git a/backend/services/backup-service/test/setup/test-database.helper.ts b/backend/services/backup-service/test/setup/test-database.helper.ts new file mode 100644 index 00000000..7e19ba61 --- /dev/null +++ b/backend/services/backup-service/test/setup/test-database.helper.ts @@ -0,0 +1,50 @@ +import { PrismaClient } from '@prisma/client'; + +let prisma: PrismaClient | null = null; + +export function getPrismaClient(): PrismaClient { + if (!prisma) { + prisma = new PrismaClient({ + datasources: { + db: { + url: process.env.DATABASE_URL, + }, + }, + }); + } + return prisma; +} + +export async function cleanDatabase(): Promise { + const client = getPrismaClient(); + + // Delete in correct order due to foreign key constraints + await client.shareAccessLog.deleteMany(); + await client.backupShare.deleteMany(); +} + +export async function disconnectDatabase(): Promise { + if (prisma) { + await prisma.$disconnect(); + prisma = null; + } +} + +export async function seedTestData(): Promise { + const client = getPrismaClient(); + + // Seed some test data if needed + await client.backupShare.create({ + data: { + userId: BigInt(999999), + accountSequence: BigInt(999999), + publicKey: '02' + 'f'.repeat(64), + partyIndex: 2, + threshold: 2, + totalParties: 3, + encryptedShareData: 'seed-encrypted-data', + encryptionKeyId: 'test-key-v1', + status: 'ACTIVE', + }, + }); +} diff --git a/backend/services/backup-service/test/tsconfig.json b/backend/services/backup-service/test/tsconfig.json new file mode 100644 index 00000000..417ec8ee --- /dev/null +++ b/backend/services/backup-service/test/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "types": ["jest", "node"] + }, + "include": ["./**/*.ts"] +} diff --git a/backend/services/backup-service/test/unit/api/backup-share.controller.spec.ts b/backend/services/backup-service/test/unit/api/backup-share.controller.spec.ts new file mode 100644 index 00000000..d13b8b3f --- /dev/null +++ b/backend/services/backup-service/test/unit/api/backup-share.controller.spec.ts @@ -0,0 +1,212 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { BackupShareController } from '../../../src/api/controllers/backup-share.controller'; +import { BackupShareApplicationService } from '../../../src/application/services/backup-share-application.service'; +import { ServiceAuthGuard } from '../../../src/shared/guards/service-auth.guard'; +import { ConfigService } from '@nestjs/config'; +import { + createStoreSharePayload, + createRetrieveSharePayload, + createRevokeSharePayload, + generatePublicKey, +} from '../../utils/test-utils'; + +describe('BackupShareController', () => { + let controller: BackupShareController; + let mockApplicationService: jest.Mocked; + + const mockRequest = { + sourceService: 'identity-service', + sourceIp: '127.0.0.1', + }; + + beforeEach(async () => { + mockApplicationService = { + storeBackupShare: jest.fn(), + getBackupShare: jest.fn(), + revokeShare: jest.fn(), + } as any; + + const module: TestingModule = await Test.createTestingModule({ + controllers: [BackupShareController], + providers: [ + { provide: BackupShareApplicationService, useValue: mockApplicationService }, + { + provide: ConfigService, + useValue: { + get: jest.fn().mockReturnValue('test-secret'), + }, + }, + ], + }) + .overrideGuard(ServiceAuthGuard) + .useValue({ canActivate: () => true }) + .compile(); + + controller = module.get(BackupShareController); + }); + + describe('storeShare', () => { + it('should store a backup share successfully', async () => { + const dto = createStoreSharePayload(); + mockApplicationService.storeBackupShare.mockResolvedValue({ shareId: '1' }); + + const result = await controller.storeShare(dto, mockRequest); + + expect(result.success).toBe(true); + expect(result.shareId).toBe('1'); + expect(result.message).toBe('Backup share stored successfully'); + expect(mockApplicationService.storeBackupShare).toHaveBeenCalledWith( + expect.objectContaining({ + userId: dto.userId, + accountSequence: dto.accountSequence, + publicKey: dto.publicKey, + encryptedShareData: dto.encryptedShareData, + sourceService: 'identity-service', + sourceIp: '127.0.0.1', + }), + ); + }); + + it('should pass optional threshold and totalParties', async () => { + const dto = createStoreSharePayload({ + threshold: 3, + totalParties: 5, + }); + mockApplicationService.storeBackupShare.mockResolvedValue({ shareId: '2' }); + + await controller.storeShare(dto, mockRequest); + + expect(mockApplicationService.storeBackupShare).toHaveBeenCalledWith( + expect.objectContaining({ + threshold: 3, + totalParties: 5, + }), + ); + }); + + it('should handle different user IDs', async () => { + const dto = createStoreSharePayload({ userId: '999888777' }); + mockApplicationService.storeBackupShare.mockResolvedValue({ shareId: '3' }); + + await controller.storeShare(dto, mockRequest); + + expect(mockApplicationService.storeBackupShare).toHaveBeenCalledWith( + expect.objectContaining({ + userId: '999888777', + }), + ); + }); + }); + + describe('retrieveShare', () => { + it('should retrieve a backup share successfully', async () => { + const dto = createRetrieveSharePayload(); + mockApplicationService.getBackupShare.mockResolvedValue({ + encryptedShareData: 'decrypted-data', + partyIndex: 2, + publicKey: dto.publicKey, + }); + + const result = await controller.retrieveShare(dto, mockRequest); + + expect(result.success).toBe(true); + expect(result.encryptedShareData).toBe('decrypted-data'); + expect(result.partyIndex).toBe(2); + expect(result.publicKey).toBe(dto.publicKey); + }); + + it('should pass deviceId when provided', async () => { + const dto = createRetrieveSharePayload({ deviceId: 'device-123' }); + mockApplicationService.getBackupShare.mockResolvedValue({ + encryptedShareData: 'data', + partyIndex: 2, + publicKey: dto.publicKey, + }); + + await controller.retrieveShare(dto, mockRequest); + + expect(mockApplicationService.getBackupShare).toHaveBeenCalledWith( + expect.objectContaining({ + deviceId: 'device-123', + }), + ); + }); + + it('should include recovery token in query', async () => { + const dto = createRetrieveSharePayload({ recoveryToken: 'special-token-123' }); + mockApplicationService.getBackupShare.mockResolvedValue({ + encryptedShareData: 'data', + partyIndex: 2, + publicKey: dto.publicKey, + }); + + await controller.retrieveShare(dto, mockRequest); + + expect(mockApplicationService.getBackupShare).toHaveBeenCalledWith( + expect.objectContaining({ + recoveryToken: 'special-token-123', + }), + ); + }); + }); + + describe('revokeShare', () => { + it('should revoke a backup share successfully', async () => { + const dto = createRevokeSharePayload(); + mockApplicationService.revokeShare.mockResolvedValue(undefined); + + const result = await controller.revokeShare(dto, mockRequest); + + expect(result.success).toBe(true); + expect(result.message).toBe('Backup share revoked successfully'); + }); + + it('should pass correct reason to service', async () => { + const dto = createRevokeSharePayload({ reason: 'SECURITY_BREACH' }); + mockApplicationService.revokeShare.mockResolvedValue(undefined); + + await controller.revokeShare(dto, mockRequest); + + expect(mockApplicationService.revokeShare).toHaveBeenCalledWith( + expect.objectContaining({ + reason: 'SECURITY_BREACH', + }), + ); + }); + + it('should handle different revoke reasons', async () => { + const reasons = ['ROTATION', 'ACCOUNT_CLOSED', 'SECURITY_BREACH', 'USER_REQUEST']; + + for (const reason of reasons) { + const dto = createRevokeSharePayload({ + reason, + publicKey: generatePublicKey(reason[0].toLowerCase()), + }); + mockApplicationService.revokeShare.mockResolvedValue(undefined); + + await controller.revokeShare(dto, mockRequest); + + expect(mockApplicationService.revokeShare).toHaveBeenCalledWith( + expect.objectContaining({ reason }), + ); + } + }); + }); + + describe('request context', () => { + it('should extract source service from request', async () => { + const dto = createStoreSharePayload(); + const customRequest = { sourceService: 'recovery-service', sourceIp: '10.0.0.1' }; + mockApplicationService.storeBackupShare.mockResolvedValue({ shareId: '1' }); + + await controller.storeShare(dto, customRequest); + + expect(mockApplicationService.storeBackupShare).toHaveBeenCalledWith( + expect.objectContaining({ + sourceService: 'recovery-service', + sourceIp: '10.0.0.1', + }), + ); + }); + }); +}); diff --git a/backend/services/backup-service/test/unit/api/health.controller.spec.ts b/backend/services/backup-service/test/unit/api/health.controller.spec.ts new file mode 100644 index 00000000..55db9add --- /dev/null +++ b/backend/services/backup-service/test/unit/api/health.controller.spec.ts @@ -0,0 +1,88 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { HealthController } from '../../../src/api/controllers/health.controller'; +import { PrismaService } from '../../../src/infrastructure/persistence/prisma/prisma.service'; + +describe('HealthController', () => { + let controller: HealthController; + let mockPrisma: { $queryRaw: jest.Mock }; + + beforeEach(async () => { + mockPrisma = { + $queryRaw: jest.fn(), + }; + + const module: TestingModule = await Test.createTestingModule({ + controllers: [HealthController], + providers: [{ provide: PrismaService, useValue: mockPrisma }], + }).compile(); + + controller = module.get(HealthController); + }); + + describe('check', () => { + it('should return ok status', async () => { + const result = await controller.check(); + + expect(result.status).toBe('ok'); + expect(result.service).toBe('backup-service'); + expect(result.timestamp).toBeDefined(); + }); + + it('should return valid timestamp', async () => { + const result = await controller.check(); + + const timestamp = new Date(result.timestamp); + expect(timestamp.getTime()).not.toBeNaN(); + expect(timestamp.getTime()).toBeLessThanOrEqual(Date.now()); + }); + }); + + describe('readiness', () => { + it('should return ready when database is connected', async () => { + mockPrisma.$queryRaw.mockResolvedValue([{ 1: 1 }]); + + const result = await controller.readiness(); + + expect(result.status).toBe('ready'); + expect(result.database).toBe('connected'); + expect(result.timestamp).toBeDefined(); + }); + + it('should return not ready when database is disconnected', async () => { + mockPrisma.$queryRaw.mockRejectedValue(new Error('Connection refused')); + + const result = await controller.readiness(); + + expect(result.status).toBe('not ready'); + expect(result.database).toBe('disconnected'); + expect(result.error).toBe('Connection refused'); + }); + + it('should handle timeout errors', async () => { + mockPrisma.$queryRaw.mockRejectedValue(new Error('Query timeout')); + + const result = await controller.readiness(); + + expect(result.status).toBe('not ready'); + expect(result.error).toBe('Query timeout'); + }); + }); + + describe('liveness', () => { + it('should return alive status', async () => { + const result = await controller.liveness(); + + expect(result.status).toBe('alive'); + expect(result.timestamp).toBeDefined(); + }); + + it('should always succeed regardless of database state', async () => { + // Even if database check would fail, liveness should succeed + mockPrisma.$queryRaw.mockRejectedValue(new Error('DB Error')); + + const result = await controller.liveness(); + + expect(result.status).toBe('alive'); + }); + }); +}); diff --git a/backend/services/backup-service/test/unit/application/get-backup-share.handler.spec.ts b/backend/services/backup-service/test/unit/application/get-backup-share.handler.spec.ts new file mode 100644 index 00000000..88207a2f --- /dev/null +++ b/backend/services/backup-service/test/unit/application/get-backup-share.handler.spec.ts @@ -0,0 +1,134 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { ConfigService } from '@nestjs/config'; +import { GetBackupShareHandler } from '../../../src/application/queries/get-backup-share/get-backup-share.handler'; +import { GetBackupShareQuery } from '../../../src/application/queries/get-backup-share/get-backup-share.query'; +import { BACKUP_SHARE_REPOSITORY, BackupShare, BackupShareStatus } from '../../../src/domain'; +import { AesEncryptionService } from '../../../src/infrastructure/crypto/aes-encryption.service'; +import { AuditLogRepository } from '../../../src/infrastructure/persistence/repositories/audit-log.repository'; +import { ApplicationError } from '../../../src/application/errors/application.error'; + +describe('GetBackupShareHandler', () => { + let handler: GetBackupShareHandler; + let mockRepository: any; + let mockEncryptionService: any; + let mockAuditLogRepository: any; + + const createMockShare = (status: BackupShareStatus = BackupShareStatus.ACTIVE) => { + const share = BackupShare.create({ + userId: BigInt(12345), + accountSequence: BigInt(1001), + publicKey: '02' + 'a'.repeat(64), + encryptedShareData: 'double-encrypted-data', + encryptionKeyId: 'key-v1', + }); + share.setShareId(BigInt(1)); + if (status === BackupShareStatus.REVOKED) { + share.revoke('TEST'); + } + return share; + }; + + beforeEach(async () => { + mockRepository = { + findByUserIdAndPublicKey: jest.fn(), + save: jest.fn().mockImplementation((share) => Promise.resolve(share)), + }; + + mockEncryptionService = { + decrypt: jest.fn().mockResolvedValue('original-encrypted-data'), + }; + + mockAuditLogRepository = { + log: jest.fn().mockResolvedValue(undefined), + countRetrievesByUserToday: jest.fn().mockResolvedValue(0), + }; + + const module: TestingModule = await Test.createTestingModule({ + providers: [ + GetBackupShareHandler, + { provide: BACKUP_SHARE_REPOSITORY, useValue: mockRepository }, + { provide: AesEncryptionService, useValue: mockEncryptionService }, + { provide: AuditLogRepository, useValue: mockAuditLogRepository }, + { + provide: ConfigService, + useValue: { + get: (key: string) => { + if (key === 'MAX_RETRIEVE_PER_DAY') return 3; + return undefined; + }, + }, + }, + ], + }).compile(); + + handler = module.get(GetBackupShareHandler); + }); + + it('should be defined', () => { + expect(handler).toBeDefined(); + }); + + describe('execute', () => { + const validQuery = new GetBackupShareQuery( + '12345', + '02' + 'a'.repeat(64), + 'valid-recovery-token', + 'identity-service', + '127.0.0.1', + ); + + it('should retrieve backup share successfully', async () => { + const mockShare = createMockShare(); + mockRepository.findByUserIdAndPublicKey.mockResolvedValue(mockShare); + + const result = await handler.execute(validQuery); + + expect(result.encryptedShareData).toBe('original-encrypted-data'); + expect(result.partyIndex).toBe(2); + expect(result.publicKey).toBe(validQuery.publicKey); + expect(mockEncryptionService.decrypt).toHaveBeenCalledWith( + 'double-encrypted-data', + 'key-v1', + ); + expect(mockAuditLogRepository.log).toHaveBeenCalledWith( + expect.objectContaining({ + action: 'RETRIEVE', + success: true, + }), + ); + }); + + it('should throw error if share not found', async () => { + mockRepository.findByUserIdAndPublicKey.mockResolvedValue(null); + + await expect(handler.execute(validQuery)).rejects.toThrow(ApplicationError); + await expect(handler.execute(validQuery)).rejects.toThrow('Backup share not found'); + }); + + it('should throw error if share is not active', async () => { + const revokedShare = createMockShare(BackupShareStatus.REVOKED); + mockRepository.findByUserIdAndPublicKey.mockResolvedValue(revokedShare); + + await expect(handler.execute(validQuery)).rejects.toThrow(ApplicationError); + await expect(handler.execute(validQuery)).rejects.toThrow('Backup share is not active'); + }); + + it('should throw error if rate limit exceeded', async () => { + mockAuditLogRepository.countRetrievesByUserToday.mockResolvedValue(3); + + await expect(handler.execute(validQuery)).rejects.toThrow(ApplicationError); + await expect(handler.execute(validQuery)).rejects.toThrow('Rate limit exceeded'); + }); + + it('should increment access count', async () => { + const mockShare = createMockShare(); + const initialAccessCount = mockShare.accessCount; + mockRepository.findByUserIdAndPublicKey.mockResolvedValue(mockShare); + + await handler.execute(validQuery); + + expect(mockShare.accessCount).toBe(initialAccessCount + 1); + expect(mockRepository.save).toHaveBeenCalled(); + }); + }); +}); diff --git a/backend/services/backup-service/test/unit/application/store-backup-share.handler.spec.ts b/backend/services/backup-service/test/unit/application/store-backup-share.handler.spec.ts new file mode 100644 index 00000000..2266bf83 --- /dev/null +++ b/backend/services/backup-service/test/unit/application/store-backup-share.handler.spec.ts @@ -0,0 +1,127 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { StoreBackupShareHandler } from '../../../src/application/commands/store-backup-share/store-backup-share.handler'; +import { StoreBackupShareCommand } from '../../../src/application/commands/store-backup-share/store-backup-share.command'; +import { BACKUP_SHARE_REPOSITORY } from '../../../src/domain'; +import { AesEncryptionService } from '../../../src/infrastructure/crypto/aes-encryption.service'; +import { AuditLogRepository } from '../../../src/infrastructure/persistence/repositories/audit-log.repository'; +import { ApplicationError } from '../../../src/application/errors/application.error'; + +describe('StoreBackupShareHandler', () => { + let handler: StoreBackupShareHandler; + let mockRepository: any; + let mockEncryptionService: any; + let mockAuditLogRepository: any; + + beforeEach(async () => { + mockRepository = { + findByUserId: jest.fn(), + findByPublicKey: jest.fn(), + save: jest.fn(), + }; + + mockEncryptionService = { + encrypt: jest.fn().mockResolvedValue({ + encrypted: 'double-encrypted-data', + keyId: 'key-v1', + }), + }; + + mockAuditLogRepository = { + log: jest.fn().mockResolvedValue(undefined), + }; + + const module: TestingModule = await Test.createTestingModule({ + providers: [ + StoreBackupShareHandler, + { provide: BACKUP_SHARE_REPOSITORY, useValue: mockRepository }, + { provide: AesEncryptionService, useValue: mockEncryptionService }, + { provide: AuditLogRepository, useValue: mockAuditLogRepository }, + ], + }).compile(); + + handler = module.get(StoreBackupShareHandler); + }); + + it('should be defined', () => { + expect(handler).toBeDefined(); + }); + + describe('execute', () => { + const validCommand = new StoreBackupShareCommand( + '12345', + 1001, + '02' + 'a'.repeat(64), + 'encrypted-share-data', + 'identity-service', + '127.0.0.1', + ); + + it('should store backup share successfully', async () => { + mockRepository.findByUserId.mockResolvedValue(null); + mockRepository.findByPublicKey.mockResolvedValue(null); + mockRepository.save.mockImplementation((share: any) => { + share.setShareId(BigInt(1)); + return Promise.resolve(share); + }); + + const result = await handler.execute(validCommand); + + expect(result.shareId).toBe('1'); + expect(mockRepository.findByUserId).toHaveBeenCalledWith(BigInt(12345)); + expect(mockRepository.findByPublicKey).toHaveBeenCalledWith(validCommand.publicKey); + expect(mockEncryptionService.encrypt).toHaveBeenCalledWith(validCommand.encryptedShareData); + expect(mockRepository.save).toHaveBeenCalled(); + expect(mockAuditLogRepository.log).toHaveBeenCalledWith( + expect.objectContaining({ + action: 'STORE', + success: true, + }), + ); + }); + + it('should throw error if share already exists for user', async () => { + mockRepository.findByUserId.mockResolvedValue({ userId: BigInt(12345) }); + + await expect(handler.execute(validCommand)).rejects.toThrow(ApplicationError); + await expect(handler.execute(validCommand)).rejects.toThrow( + 'Backup share already exists for this user', + ); + }); + + it('should throw error if share already exists for public key', async () => { + mockRepository.findByUserId.mockResolvedValue(null); + mockRepository.findByPublicKey.mockResolvedValue({ publicKey: validCommand.publicKey }); + + await expect(handler.execute(validCommand)).rejects.toThrow(ApplicationError); + await expect(handler.execute(validCommand)).rejects.toThrow( + 'Backup share already exists for this public key', + ); + }); + + it('should use custom threshold and totalParties if provided', async () => { + mockRepository.findByUserId.mockResolvedValue(null); + mockRepository.findByPublicKey.mockResolvedValue(null); + mockRepository.save.mockImplementation((share: any) => { + share.setShareId(BigInt(1)); + expect(share.threshold).toBe(3); + expect(share.totalParties).toBe(5); + return Promise.resolve(share); + }); + + const commandWithCustomParams = new StoreBackupShareCommand( + '12345', + 1001, + '02' + 'a'.repeat(64), + 'encrypted-share-data', + 'identity-service', + '127.0.0.1', + 3, // threshold + 5, // totalParties + ); + + await handler.execute(commandWithCustomParams); + + expect(mockRepository.save).toHaveBeenCalled(); + }); + }); +}); diff --git a/backend/services/backup-service/test/unit/domain/backup-share.entity.spec.ts b/backend/services/backup-service/test/unit/domain/backup-share.entity.spec.ts new file mode 100644 index 00000000..ae45eb44 --- /dev/null +++ b/backend/services/backup-service/test/unit/domain/backup-share.entity.spec.ts @@ -0,0 +1,170 @@ +import { + BackupShare, + BackupShareStatus, +} from '../../../src/domain/entities/backup-share.entity'; +import { DomainError } from '../../../src/domain/errors/domain.error'; + +describe('BackupShare Entity', () => { + const validParams = { + userId: BigInt(12345), + accountSequence: BigInt(1001), + publicKey: '02' + 'a'.repeat(64), + encryptedShareData: 'encrypted-data-base64', + encryptionKeyId: 'key-v1', + }; + + describe('create', () => { + it('should create a backup share with valid parameters', () => { + const share = BackupShare.create(validParams); + + expect(share.userId).toBe(validParams.userId); + expect(share.accountSequence).toBe(validParams.accountSequence); + expect(share.publicKey).toBe(validParams.publicKey); + expect(share.encryptedShareData).toBe(validParams.encryptedShareData); + expect(share.encryptionKeyId).toBe(validParams.encryptionKeyId); + expect(share.partyIndex).toBe(2); + expect(share.threshold).toBe(2); + expect(share.totalParties).toBe(3); + expect(share.status).toBe(BackupShareStatus.ACTIVE); + expect(share.accessCount).toBe(0); + expect(share.shareId).toBeNull(); + }); + + it('should allow custom threshold and totalParties', () => { + const share = BackupShare.create({ + ...validParams, + threshold: 3, + totalParties: 5, + }); + + expect(share.threshold).toBe(3); + expect(share.totalParties).toBe(5); + }); + }); + + describe('recordAccess', () => { + it('should increment access count for active share', () => { + const share = BackupShare.create(validParams); + + share.recordAccess(); + + expect(share.accessCount).toBe(1); + expect(share.lastAccessedAt).not.toBeNull(); + }); + + it('should throw error when accessing revoked share', () => { + const share = BackupShare.create(validParams); + share.revoke('TEST_REASON'); + + expect(() => share.recordAccess()).toThrow(DomainError); + expect(() => share.recordAccess()).toThrow( + 'Cannot access revoked or rotated share', + ); + }); + }); + + describe('revoke', () => { + it('should revoke an active share', () => { + const share = BackupShare.create(validParams); + + share.revoke('SECURITY_BREACH'); + + expect(share.status).toBe(BackupShareStatus.REVOKED); + expect(share.revokedAt).not.toBeNull(); + }); + + it('should throw error when revoking already revoked share', () => { + const share = BackupShare.create(validParams); + share.revoke('FIRST_REVOKE'); + + expect(() => share.revoke('SECOND_REVOKE')).toThrow(DomainError); + expect(() => share.revoke('SECOND_REVOKE')).toThrow( + 'Share already revoked', + ); + }); + }); + + describe('rotate', () => { + it('should rotate encryption for active share', () => { + const share = BackupShare.create(validParams); + const newEncryptedData = 'new-encrypted-data'; + const newKeyId = 'key-v2'; + + share.rotate(newEncryptedData, newKeyId); + + expect(share.encryptedShareData).toBe(newEncryptedData); + expect(share.encryptionKeyId).toBe(newKeyId); + expect(share.status).toBe(BackupShareStatus.ACTIVE); + }); + + it('should throw error when rotating revoked share', () => { + const share = BackupShare.create(validParams); + share.revoke('TEST'); + + expect(() => share.rotate('new-data', 'key-v2')).toThrow(DomainError); + expect(() => share.rotate('new-data', 'key-v2')).toThrow( + 'Cannot rotate revoked share', + ); + }); + }); + + describe('isActive', () => { + it('should return true for active share', () => { + const share = BackupShare.create(validParams); + + expect(share.isActive()).toBe(true); + }); + + it('should return false for revoked share', () => { + const share = BackupShare.create(validParams); + share.revoke('TEST'); + + expect(share.isActive()).toBe(false); + }); + }); + + describe('setShareId', () => { + it('should set share ID when not already set', () => { + const share = BackupShare.create(validParams); + + share.setShareId(BigInt(1)); + + expect(share.shareId).toBe(BigInt(1)); + }); + + it('should throw error when share ID already set', () => { + const share = BackupShare.create(validParams); + share.setShareId(BigInt(1)); + + expect(() => share.setShareId(BigInt(2))).toThrow(DomainError); + expect(() => share.setShareId(BigInt(2))).toThrow('Share ID already set'); + }); + }); + + describe('toProps', () => { + it('should return all properties', () => { + const share = BackupShare.create(validParams); + const props = share.toProps(); + + expect(props.userId).toBe(validParams.userId); + expect(props.accountSequence).toBe(validParams.accountSequence); + expect(props.publicKey).toBe(validParams.publicKey); + expect(props.status).toBe(BackupShareStatus.ACTIVE); + expect(props.partyIndex).toBe(2); + }); + }); + + describe('reconstitute', () => { + it('should reconstitute from props', () => { + const original = BackupShare.create(validParams); + original.setShareId(BigInt(123)); + const props = original.toProps(); + + const reconstituted = BackupShare.reconstitute(props); + + expect(reconstituted.shareId).toBe(props.shareId); + expect(reconstituted.userId).toBe(props.userId); + expect(reconstituted.status).toBe(props.status); + }); + }); +}); diff --git a/backend/services/backup-service/test/unit/domain/value-objects.spec.ts b/backend/services/backup-service/test/unit/domain/value-objects.spec.ts new file mode 100644 index 00000000..cc88ba7f --- /dev/null +++ b/backend/services/backup-service/test/unit/domain/value-objects.spec.ts @@ -0,0 +1,178 @@ +import { ShareId } from '../../../src/domain/value-objects/share-id.vo'; +import { EncryptedData } from '../../../src/domain/value-objects/encrypted-data.vo'; +import { DomainError } from '../../../src/domain/errors/domain.error'; + +describe('Value Objects', () => { + describe('ShareId', () => { + describe('create', () => { + it('should create ShareId from bigint', () => { + const shareId = ShareId.create(BigInt(123)); + expect(shareId.value).toBe(BigInt(123)); + }); + + it('should create ShareId from string', () => { + const shareId = ShareId.create('456'); + expect(shareId.value).toBe(BigInt(456)); + }); + + it('should create ShareId from number', () => { + const shareId = ShareId.create(789); + expect(shareId.value).toBe(BigInt(789)); + }); + + it('should throw error for zero', () => { + expect(() => ShareId.create(0)).toThrow(DomainError); + expect(() => ShareId.create(0)).toThrow('ShareId must be a positive number'); + }); + + it('should throw error for negative number', () => { + expect(() => ShareId.create(-1)).toThrow(DomainError); + expect(() => ShareId.create(BigInt(-100))).toThrow(DomainError); + }); + }); + + describe('toString', () => { + it('should return string representation', () => { + const shareId = ShareId.create(12345); + expect(shareId.toString()).toBe('12345'); + }); + }); + + describe('equals', () => { + it('should return true for equal values', () => { + const id1 = ShareId.create(100); + const id2 = ShareId.create(100); + expect(id1.equals(id2)).toBe(true); + }); + + it('should return false for different values', () => { + const id1 = ShareId.create(100); + const id2 = ShareId.create(200); + expect(id1.equals(id2)).toBe(false); + }); + }); + }); + + describe('EncryptedData', () => { + describe('create', () => { + it('should create EncryptedData with valid params', () => { + const data = EncryptedData.create({ + ciphertext: 'encrypted-content', + iv: 'initialization-vector', + authTag: 'authentication-tag', + keyId: 'key-v1', + }); + + expect(data.ciphertext).toBe('encrypted-content'); + expect(data.iv).toBe('initialization-vector'); + expect(data.authTag).toBe('authentication-tag'); + expect(data.keyId).toBe('key-v1'); + }); + + it('should throw error for empty ciphertext', () => { + expect(() => + EncryptedData.create({ + ciphertext: '', + iv: 'iv', + authTag: 'tag', + keyId: 'key', + }), + ).toThrow(DomainError); + expect(() => + EncryptedData.create({ + ciphertext: '', + iv: 'iv', + authTag: 'tag', + keyId: 'key', + }), + ).toThrow('Ciphertext cannot be empty'); + }); + + it('should throw error for empty IV', () => { + expect(() => + EncryptedData.create({ + ciphertext: 'cipher', + iv: '', + authTag: 'tag', + keyId: 'key', + }), + ).toThrow('IV cannot be empty'); + }); + + it('should throw error for empty auth tag', () => { + expect(() => + EncryptedData.create({ + ciphertext: 'cipher', + iv: 'iv', + authTag: '', + keyId: 'key', + }), + ).toThrow('Auth tag cannot be empty'); + }); + + it('should throw error for empty key ID', () => { + expect(() => + EncryptedData.create({ + ciphertext: 'cipher', + iv: 'iv', + authTag: 'tag', + keyId: '', + }), + ).toThrow('Key ID cannot be empty'); + }); + }); + + describe('fromSerializedString', () => { + it('should parse valid serialized string', () => { + const serialized = 'ciphertext:iv:authTag'; + const data = EncryptedData.fromSerializedString(serialized, 'key-v1'); + + expect(data.ciphertext).toBe('ciphertext'); + expect(data.iv).toBe('iv'); + expect(data.authTag).toBe('authTag'); + expect(data.keyId).toBe('key-v1'); + }); + + it('should throw error for invalid format (too few parts)', () => { + expect(() => EncryptedData.fromSerializedString('only:two', 'key')).toThrow( + 'Invalid encrypted data format', + ); + }); + + it('should throw error for invalid format (too many parts)', () => { + expect(() => + EncryptedData.fromSerializedString('one:two:three:four', 'key'), + ).toThrow('Invalid encrypted data format'); + }); + }); + + describe('toSerializedString', () => { + it('should serialize to correct format', () => { + const data = EncryptedData.create({ + ciphertext: 'cipher123', + iv: 'iv456', + authTag: 'tag789', + keyId: 'key-v2', + }); + + expect(data.toSerializedString()).toBe('cipher123:iv456:tag789'); + }); + + it('should be reversible', () => { + const original = EncryptedData.create({ + ciphertext: 'test-cipher', + iv: 'test-iv', + authTag: 'test-tag', + keyId: 'key-v1', + }); + + const serialized = original.toSerializedString(); + const restored = EncryptedData.fromSerializedString(serialized, 'key-v1'); + + expect(restored.ciphertext).toBe(original.ciphertext); + expect(restored.iv).toBe(original.iv); + expect(restored.authTag).toBe(original.authTag); + }); + }); + }); +}); diff --git a/backend/services/backup-service/test/unit/infrastructure/aes-encryption.service.spec.ts b/backend/services/backup-service/test/unit/infrastructure/aes-encryption.service.spec.ts new file mode 100644 index 00000000..6922cb0d --- /dev/null +++ b/backend/services/backup-service/test/unit/infrastructure/aes-encryption.service.spec.ts @@ -0,0 +1,142 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { ConfigService } from '@nestjs/config'; +import { AesEncryptionService } from '../../../src/infrastructure/crypto/aes-encryption.service'; + +describe('AesEncryptionService', () => { + let service: AesEncryptionService; + + const testKey = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef'; + const testKeyId = 'test-key-v1'; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + AesEncryptionService, + { + provide: ConfigService, + useValue: { + get: (key: string) => { + switch (key) { + case 'BACKUP_ENCRYPTION_KEY': + return testKey; + case 'BACKUP_ENCRYPTION_KEY_ID': + return testKeyId; + default: + return undefined; + } + }, + }, + }, + ], + }).compile(); + + service = module.get(AesEncryptionService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + describe('encrypt', () => { + it('should encrypt plaintext successfully', async () => { + const plaintext = 'Hello, World!'; + + const result = await service.encrypt(plaintext); + + expect(result.encrypted).toBeDefined(); + expect(result.keyId).toBe(testKeyId); + expect(result.encrypted).not.toBe(plaintext); + // Should contain 3 parts separated by ':' + expect(result.encrypted.split(':').length).toBe(3); + }); + + it('should produce different ciphertext for same plaintext (due to random IV)', async () => { + const plaintext = 'Same message'; + + const result1 = await service.encrypt(plaintext); + const result2 = await service.encrypt(plaintext); + + expect(result1.encrypted).not.toBe(result2.encrypted); + }); + }); + + describe('decrypt', () => { + it('should decrypt ciphertext back to original plaintext', async () => { + const plaintext = 'Secret message for testing'; + + const { encrypted, keyId } = await service.encrypt(plaintext); + const decrypted = await service.decrypt(encrypted, keyId); + + expect(decrypted).toBe(plaintext); + }); + + it('should handle special characters', async () => { + const plaintext = 'Special chars: !@#$%^&*()_+-={}[]|\\:";\'<>?,./'; + + const { encrypted, keyId } = await service.encrypt(plaintext); + const decrypted = await service.decrypt(encrypted, keyId); + + expect(decrypted).toBe(plaintext); + }); + + it('should handle unicode characters', async () => { + const plaintext = 'Unicode: 你好世界 🌍 مرحبا'; + + const { encrypted, keyId } = await service.encrypt(plaintext); + const decrypted = await service.decrypt(encrypted, keyId); + + expect(decrypted).toBe(plaintext); + }); + + it('should handle large payloads', async () => { + const plaintext = 'a'.repeat(10000); + + const { encrypted, keyId } = await service.encrypt(plaintext); + const decrypted = await service.decrypt(encrypted, keyId); + + expect(decrypted).toBe(plaintext); + }); + + it('should throw error for invalid format', async () => { + await expect(service.decrypt('invalid-format', testKeyId)).rejects.toThrow( + 'Invalid encrypted data format', + ); + }); + + it('should throw error for non-existent key', async () => { + const { encrypted } = await service.encrypt('test'); + + await expect(service.decrypt(encrypted, 'non-existent-key')).rejects.toThrow( + 'Decryption key not found', + ); + }); + }); + + describe('addKey', () => { + it('should add new key for rotation', async () => { + const newKeyId = 'key-v2'; + const newKey = 'fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210'; + + service.addKey(newKeyId, newKey); + + // Encrypt with original key + const { encrypted: encrypted1 } = await service.encrypt('test'); + + // Should still be able to decrypt (uses current key) + const decrypted = await service.decrypt(encrypted1, testKeyId); + expect(decrypted).toBe('test'); + }); + + it('should throw error for invalid key length', () => { + expect(() => service.addKey('bad-key', 'too-short')).toThrow( + 'Key must be 256 bits', + ); + }); + }); + + describe('getCurrentKeyId', () => { + it('should return current key ID', () => { + expect(service.getCurrentKeyId()).toBe(testKeyId); + }); + }); +}); diff --git a/backend/services/backup-service/test/unit/shared/audit-log.interceptor.spec.ts b/backend/services/backup-service/test/unit/shared/audit-log.interceptor.spec.ts new file mode 100644 index 00000000..c41a7550 --- /dev/null +++ b/backend/services/backup-service/test/unit/shared/audit-log.interceptor.spec.ts @@ -0,0 +1,192 @@ +import { ExecutionContext, CallHandler } from '@nestjs/common'; +import { of, throwError } from 'rxjs'; +import { AuditLogInterceptor } from '../../../src/shared/interceptors/audit-log.interceptor'; + +describe('AuditLogInterceptor', () => { + let interceptor: AuditLogInterceptor; + + const createMockContext = ( + method: string = 'POST', + url: string = '/backup-share/store', + body: any = {}, + sourceService: string = 'identity-service', + sourceIp: string = '127.0.0.1', + ): ExecutionContext => { + return { + switchToHttp: () => ({ + getRequest: () => ({ + method, + url, + body, + sourceService, + sourceIp, + }), + }), + } as ExecutionContext; + }; + + const createMockCallHandler = (response: any = {}): CallHandler => ({ + handle: () => of(response), + }); + + const createErrorCallHandler = (error: Error): CallHandler => ({ + handle: () => throwError(() => error), + }); + + beforeEach(() => { + interceptor = new AuditLogInterceptor(); + }); + + describe('intercept', () => { + it('should allow request to proceed', (done) => { + const context = createMockContext(); + const handler = createMockCallHandler({ success: true }); + + interceptor.intercept(context, handler).subscribe({ + next: (response) => { + expect(response).toEqual({ success: true }); + done(); + }, + }); + }); + + it('should not modify the response', (done) => { + const context = createMockContext(); + const originalResponse = { shareId: '123', success: true }; + const handler = createMockCallHandler(originalResponse); + + interceptor.intercept(context, handler).subscribe({ + next: (response) => { + expect(response).toEqual(originalResponse); + done(); + }, + }); + }); + + it('should propagate errors', (done) => { + const context = createMockContext(); + const error = new Error('Test error'); + const handler = createErrorCallHandler(error); + + interceptor.intercept(context, handler).subscribe({ + error: (err) => { + expect(err).toBe(error); + done(); + }, + }); + }); + + it('should sanitize sensitive fields in body', () => { + const sensitiveBody = { + userId: '12345', + encryptedShareData: 'secret-data', + recoveryToken: 'secret-token', + password: 'secret-password', + publicKey: '02abc...', + }; + + // Access the private method through prototype + const sanitized = (interceptor as any).sanitizeBody(sensitiveBody); + + expect(sanitized.userId).toBe('12345'); + expect(sanitized.publicKey).toBe('02abc...'); + expect(sanitized.encryptedShareData).toBe('[REDACTED]'); + expect(sanitized.recoveryToken).toBe('[REDACTED]'); + expect(sanitized.password).toBe('[REDACTED]'); + }); + + it('should handle null body', () => { + const sanitized = (interceptor as any).sanitizeBody(null); + expect(sanitized).toBeNull(); + }); + + it('should handle undefined body', () => { + const sanitized = (interceptor as any).sanitizeBody(undefined); + expect(sanitized).toBeUndefined(); + }); + + it('should handle empty body', () => { + const sanitized = (interceptor as any).sanitizeBody({}); + expect(sanitized).toEqual({}); + }); + + it('should not modify original body object', () => { + const originalBody = { + userId: '12345', + encryptedShareData: 'secret-data', + }; + const bodyCopy = { ...originalBody }; + + (interceptor as any).sanitizeBody(originalBody); + + expect(originalBody).toEqual(bodyCopy); + }); + }); + + describe('logging behavior', () => { + it('should handle different HTTP methods', (done) => { + const methods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH']; + + let completed = 0; + methods.forEach((method) => { + const context = createMockContext(method, '/test'); + const handler = createMockCallHandler(); + + interceptor.intercept(context, handler).subscribe({ + complete: () => { + completed++; + if (completed === methods.length) { + done(); + } + }, + }); + }); + }); + + it('should handle different URL paths', (done) => { + const paths = [ + '/backup-share/store', + '/backup-share/retrieve', + '/backup-share/revoke', + '/health', + ]; + + let completed = 0; + paths.forEach((url) => { + const context = createMockContext('GET', url); + const handler = createMockCallHandler(); + + interceptor.intercept(context, handler).subscribe({ + complete: () => { + completed++; + if (completed === paths.length) { + done(); + } + }, + }); + }); + }); + + it('should handle unknown source service', (done) => { + const context = createMockContext('POST', '/test', {}, undefined, '127.0.0.1'); + const handler = createMockCallHandler(); + + interceptor.intercept(context, handler).subscribe({ + complete: () => { + done(); + }, + }); + }); + + it('should handle unknown source IP', (done) => { + const context = createMockContext('POST', '/test', {}, 'identity-service', undefined); + const handler = createMockCallHandler(); + + interceptor.intercept(context, handler).subscribe({ + complete: () => { + done(); + }, + }); + }); + }); +}); diff --git a/backend/services/backup-service/test/unit/shared/global-exception.filter.spec.ts b/backend/services/backup-service/test/unit/shared/global-exception.filter.spec.ts new file mode 100644 index 00000000..28fb79ff --- /dev/null +++ b/backend/services/backup-service/test/unit/shared/global-exception.filter.spec.ts @@ -0,0 +1,215 @@ +import { HttpException, HttpStatus, ArgumentsHost } from '@nestjs/common'; +import { GlobalExceptionFilter } from '../../../src/shared/filters/global-exception.filter'; +import { ApplicationError } from '../../../src/application/errors/application.error'; +import { DomainError } from '../../../src/domain/errors/domain.error'; + +describe('GlobalExceptionFilter', () => { + let filter: GlobalExceptionFilter; + let mockResponse: any; + let mockRequest: any; + let mockHost: ArgumentsHost; + + beforeEach(() => { + filter = new GlobalExceptionFilter(); + + mockResponse = { + status: jest.fn().mockReturnThis(), + json: jest.fn().mockReturnThis(), + }; + + mockRequest = { + method: 'POST', + url: '/backup-share/store', + }; + + mockHost = { + switchToHttp: () => ({ + getResponse: () => mockResponse, + getRequest: () => mockRequest, + }), + } as ArgumentsHost; + }); + + describe('catch', () => { + it('should handle HttpException', () => { + const exception = new HttpException('Bad Request', HttpStatus.BAD_REQUEST); + + filter.catch(exception, mockHost); + + expect(mockResponse.status).toHaveBeenCalledWith(HttpStatus.BAD_REQUEST); + expect(mockResponse.json).toHaveBeenCalledWith( + expect.objectContaining({ + success: false, + error: 'Bad Request', + code: 'BAD_REQUEST', + }), + ); + }); + + it('should handle HttpException with object response', () => { + const exception = new HttpException( + { message: 'Validation failed', errors: ['field is required'] }, + HttpStatus.BAD_REQUEST, + ); + + filter.catch(exception, mockHost); + + expect(mockResponse.status).toHaveBeenCalledWith(HttpStatus.BAD_REQUEST); + expect(mockResponse.json).toHaveBeenCalledWith( + expect.objectContaining({ + error: 'Validation failed', + }), + ); + }); + + it('should handle ApplicationError with SHARE_NOT_FOUND', () => { + const exception = new ApplicationError('Backup share not found', 'SHARE_NOT_FOUND'); + + filter.catch(exception, mockHost); + + expect(mockResponse.status).toHaveBeenCalledWith(HttpStatus.NOT_FOUND); + expect(mockResponse.json).toHaveBeenCalledWith( + expect.objectContaining({ + success: false, + error: 'Backup share not found', + code: 'SHARE_NOT_FOUND', + }), + ); + }); + + it('should handle ApplicationError with SHARE_ALREADY_EXISTS', () => { + const exception = new ApplicationError('Share already exists', 'SHARE_ALREADY_EXISTS'); + + filter.catch(exception, mockHost); + + expect(mockResponse.status).toHaveBeenCalledWith(HttpStatus.CONFLICT); + expect(mockResponse.json).toHaveBeenCalledWith( + expect.objectContaining({ + code: 'SHARE_ALREADY_EXISTS', + }), + ); + }); + + it('should handle ApplicationError with RATE_LIMIT_EXCEEDED', () => { + const exception = new ApplicationError('Rate limit exceeded', 'RATE_LIMIT_EXCEEDED'); + + filter.catch(exception, mockHost); + + expect(mockResponse.status).toHaveBeenCalledWith(HttpStatus.TOO_MANY_REQUESTS); + }); + + it('should handle ApplicationError with SHARE_NOT_ACTIVE', () => { + const exception = new ApplicationError('Share is not active', 'SHARE_NOT_ACTIVE'); + + filter.catch(exception, mockHost); + + expect(mockResponse.status).toHaveBeenCalledWith(HttpStatus.BAD_REQUEST); + }); + + it('should handle DomainError', () => { + const exception = new DomainError('Cannot access revoked share'); + + filter.catch(exception, mockHost); + + expect(mockResponse.status).toHaveBeenCalledWith(HttpStatus.BAD_REQUEST); + expect(mockResponse.json).toHaveBeenCalledWith( + expect.objectContaining({ + success: false, + error: 'Cannot access revoked share', + code: 'DOMAIN_ERROR', + }), + ); + }); + + it('should handle generic Error', () => { + const exception = new Error('Something went wrong'); + + filter.catch(exception, mockHost); + + expect(mockResponse.status).toHaveBeenCalledWith(HttpStatus.INTERNAL_SERVER_ERROR); + expect(mockResponse.json).toHaveBeenCalledWith( + expect.objectContaining({ + success: false, + error: 'Something went wrong', + code: 'INTERNAL_ERROR', + }), + ); + }); + + it('should handle unknown exception', () => { + const exception = 'Unknown error'; + + filter.catch(exception, mockHost); + + expect(mockResponse.status).toHaveBeenCalledWith(HttpStatus.INTERNAL_SERVER_ERROR); + expect(mockResponse.json).toHaveBeenCalledWith( + expect.objectContaining({ + code: 'INTERNAL_ERROR', + }), + ); + }); + + it('should include timestamp in response', () => { + const exception = new Error('Test'); + + filter.catch(exception, mockHost); + + expect(mockResponse.json).toHaveBeenCalledWith( + expect.objectContaining({ + timestamp: expect.any(String), + }), + ); + }); + + it('should include path in response', () => { + const exception = new Error('Test'); + + filter.catch(exception, mockHost); + + expect(mockResponse.json).toHaveBeenCalledWith( + expect.objectContaining({ + path: '/backup-share/store', + }), + ); + }); + + it('should handle UNAUTHORIZED status', () => { + const exception = new HttpException('Unauthorized', HttpStatus.UNAUTHORIZED); + + filter.catch(exception, mockHost); + + expect(mockResponse.status).toHaveBeenCalledWith(HttpStatus.UNAUTHORIZED); + expect(mockResponse.json).toHaveBeenCalledWith( + expect.objectContaining({ + code: 'UNAUTHORIZED', + }), + ); + }); + + it('should handle FORBIDDEN status', () => { + const exception = new HttpException('Forbidden', HttpStatus.FORBIDDEN); + + filter.catch(exception, mockHost); + + expect(mockResponse.status).toHaveBeenCalledWith(HttpStatus.FORBIDDEN); + expect(mockResponse.json).toHaveBeenCalledWith( + expect.objectContaining({ + code: 'FORBIDDEN', + }), + ); + }); + + it('should handle NOT_FOUND status', () => { + const exception = new HttpException('Not Found', HttpStatus.NOT_FOUND); + + filter.catch(exception, mockHost); + + expect(mockResponse.status).toHaveBeenCalledWith(HttpStatus.NOT_FOUND); + expect(mockResponse.json).toHaveBeenCalledWith( + expect.objectContaining({ + code: 'NOT_FOUND', + }), + ); + }); + }); +}); diff --git a/backend/services/backup-service/test/unit/shared/service-auth.guard.spec.ts b/backend/services/backup-service/test/unit/shared/service-auth.guard.spec.ts new file mode 100644 index 00000000..1eb270c1 --- /dev/null +++ b/backend/services/backup-service/test/unit/shared/service-auth.guard.spec.ts @@ -0,0 +1,200 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { ExecutionContext, UnauthorizedException } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { ServiceAuthGuard } from '../../../src/shared/guards/service-auth.guard'; +import { + generateServiceToken, + generateExpiredServiceToken, + TEST_SERVICE_JWT_SECRET, +} from '../../utils/test-utils'; + +describe('ServiceAuthGuard', () => { + let guard: ServiceAuthGuard; + + const createMockContext = (headers: Record = {}, connection: any = {}): ExecutionContext => { + const mockRequest = { + headers, + connection, + socket: connection, + }; + + return { + switchToHttp: () => ({ + getRequest: () => mockRequest, + }), + } as ExecutionContext; + }; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + ServiceAuthGuard, + { + provide: ConfigService, + useValue: { + get: (key: string) => { + switch (key) { + case 'SERVICE_JWT_SECRET': + return TEST_SERVICE_JWT_SECRET; + case 'ALLOWED_SERVICES': + return 'identity-service,recovery-service'; + default: + return undefined; + } + }, + }, + }, + ], + }).compile(); + + guard = module.get(ServiceAuthGuard); + }); + + describe('canActivate', () => { + it('should allow request with valid identity-service token', () => { + const token = generateServiceToken('identity-service'); + const context = createMockContext({ 'x-service-token': token }); + + expect(guard.canActivate(context)).toBe(true); + }); + + it('should allow request with valid recovery-service token', () => { + const token = generateServiceToken('recovery-service'); + const context = createMockContext({ 'x-service-token': token }); + + expect(guard.canActivate(context)).toBe(true); + }); + + it('should reject request without service token', () => { + const context = createMockContext({}); + + expect(() => guard.canActivate(context)).toThrow(UnauthorizedException); + expect(() => guard.canActivate(context)).toThrow('Missing service token'); + }); + + it('should reject request with invalid token', () => { + const context = createMockContext({ 'x-service-token': 'invalid-token' }); + + expect(() => guard.canActivate(context)).toThrow(UnauthorizedException); + expect(() => guard.canActivate(context)).toThrow('Invalid service token'); + }); + + it('should reject request with expired token', () => { + const token = generateExpiredServiceToken('identity-service'); + const context = createMockContext({ 'x-service-token': token }); + + expect(() => guard.canActivate(context)).toThrow(UnauthorizedException); + }); + + it('should reject request from unauthorized service', () => { + const token = generateServiceToken('unknown-service'); + const context = createMockContext({ 'x-service-token': token }); + + expect(() => guard.canActivate(context)).toThrow(UnauthorizedException); + expect(() => guard.canActivate(context)).toThrow('Service not authorized'); + }); + + it('should reject token signed with wrong secret', () => { + const token = generateServiceToken('identity-service', 'wrong-secret'); + const context = createMockContext({ 'x-service-token': token }); + + expect(() => guard.canActivate(context)).toThrow(UnauthorizedException); + }); + + it('should attach sourceService to request', () => { + const token = generateServiceToken('identity-service'); + const mockRequest: any = { + headers: { 'x-service-token': token }, + connection: {}, + socket: {}, + }; + const context = { + switchToHttp: () => ({ + getRequest: () => mockRequest, + }), + } as ExecutionContext; + + guard.canActivate(context); + + expect(mockRequest.sourceService).toBe('identity-service'); + }); + + it('should extract client IP from x-forwarded-for header', () => { + const token = generateServiceToken('identity-service'); + const mockRequest: any = { + headers: { + 'x-service-token': token, + 'x-forwarded-for': '203.0.113.195, 70.41.3.18, 150.172.238.178', + }, + connection: {}, + socket: {}, + }; + const context = { + switchToHttp: () => ({ + getRequest: () => mockRequest, + }), + } as ExecutionContext; + + guard.canActivate(context); + + expect(mockRequest.sourceIp).toBe('203.0.113.195'); + }); + + it('should extract client IP from x-real-ip header', () => { + const token = generateServiceToken('identity-service'); + const mockRequest: any = { + headers: { + 'x-service-token': token, + 'x-real-ip': '192.168.1.100', + }, + connection: {}, + socket: {}, + }; + const context = { + switchToHttp: () => ({ + getRequest: () => mockRequest, + }), + } as ExecutionContext; + + guard.canActivate(context); + + expect(mockRequest.sourceIp).toBe('192.168.1.100'); + }); + + it('should extract client IP from connection.remoteAddress', () => { + const token = generateServiceToken('identity-service'); + const mockRequest: any = { + headers: { 'x-service-token': token }, + connection: { remoteAddress: '10.0.0.1' }, + socket: {}, + }; + const context = { + switchToHttp: () => ({ + getRequest: () => mockRequest, + }), + } as ExecutionContext; + + guard.canActivate(context); + + expect(mockRequest.sourceIp).toBe('10.0.0.1'); + }); + + it('should return "unknown" when no IP can be determined', () => { + const token = generateServiceToken('identity-service'); + const mockRequest: any = { + headers: { 'x-service-token': token }, + connection: {}, + socket: {}, + }; + const context = { + switchToHttp: () => ({ + getRequest: () => mockRequest, + }), + } as ExecutionContext; + + guard.canActivate(context); + + expect(mockRequest.sourceIp).toBe('unknown'); + }); + }); +}); diff --git a/backend/services/backup-service/test/utils/mock-prisma.service.ts b/backend/services/backup-service/test/utils/mock-prisma.service.ts new file mode 100644 index 00000000..811759a0 --- /dev/null +++ b/backend/services/backup-service/test/utils/mock-prisma.service.ts @@ -0,0 +1,185 @@ +import { Injectable } from '@nestjs/common'; + +interface MockBackupShare { + shareId: bigint; + userId: bigint; + accountSequence: bigint; + publicKey: string; + partyIndex: number; + threshold: number; + totalParties: number; + encryptedShareData: string; + encryptionKeyId: string; + status: string; + accessCount: number; + lastAccessedAt: Date | null; + createdAt: Date; + updatedAt: Date; + revokedAt: Date | null; +} + +interface MockShareAccessLog { + logId: bigint; + shareId: bigint; + userId: bigint; + action: string; + sourceService: string; + sourceIp: string; + success: boolean; + errorMessage: string | null; + createdAt: Date; +} + +@Injectable() +export class MockPrismaService { + private backupShares: MockBackupShare[] = []; + private shareAccessLogs: MockShareAccessLog[] = []; + private shareIdCounter = 1n; + private logIdCounter = 1n; + + backupShare = { + create: jest.fn().mockImplementation(({ data }) => { + const share: MockBackupShare = { + shareId: this.shareIdCounter++, + userId: data.userId, + accountSequence: data.accountSequence, + publicKey: data.publicKey, + partyIndex: data.partyIndex ?? 2, + threshold: data.threshold ?? 2, + totalParties: data.totalParties ?? 3, + encryptedShareData: data.encryptedShareData, + encryptionKeyId: data.encryptionKeyId, + status: data.status ?? 'ACTIVE', + accessCount: data.accessCount ?? 0, + lastAccessedAt: data.lastAccessedAt ?? null, + createdAt: new Date(), + updatedAt: new Date(), + revokedAt: data.revokedAt ?? null, + }; + this.backupShares.push(share); + return Promise.resolve(share); + }), + + update: jest.fn().mockImplementation(({ where, data }) => { + const index = this.backupShares.findIndex(s => s.shareId === where.shareId); + if (index === -1) return Promise.resolve(null); + + this.backupShares[index] = { + ...this.backupShares[index], + ...data, + updatedAt: new Date(), + }; + return Promise.resolve(this.backupShares[index]); + }), + + findUnique: jest.fn().mockImplementation(({ where }) => { + let share: MockBackupShare | undefined; + if (where.shareId) { + share = this.backupShares.find(s => s.shareId === where.shareId); + } else if (where.userId) { + share = this.backupShares.find(s => s.userId === where.userId); + } else if (where.publicKey) { + share = this.backupShares.find(s => s.publicKey === where.publicKey); + } else if (where.accountSequence) { + share = this.backupShares.find(s => s.accountSequence === where.accountSequence); + } + return Promise.resolve(share ?? null); + }), + + findFirst: jest.fn().mockImplementation(({ where }) => { + const share = this.backupShares.find(s => + s.userId === where.userId && s.publicKey === where.publicKey + ); + return Promise.resolve(share ?? null); + }), + + delete: jest.fn().mockImplementation(({ where }) => { + const index = this.backupShares.findIndex(s => s.shareId === where.shareId); + if (index === -1) return Promise.resolve(null); + const deleted = this.backupShares.splice(index, 1)[0]; + return Promise.resolve(deleted); + }), + + deleteMany: jest.fn().mockImplementation(() => { + const count = this.backupShares.length; + this.backupShares = []; + return Promise.resolve({ count }); + }), + }; + + shareAccessLog = { + create: jest.fn().mockImplementation(({ data }) => { + const log: MockShareAccessLog = { + logId: this.logIdCounter++, + shareId: data.shareId, + userId: data.userId, + action: data.action, + sourceService: data.sourceService, + sourceIp: data.sourceIp, + success: data.success ?? true, + errorMessage: data.errorMessage ?? null, + createdAt: new Date(), + }; + this.shareAccessLogs.push(log); + return Promise.resolve(log); + }), + + findMany: jest.fn().mockImplementation(({ where, orderBy, take }) => { + let logs = [...this.shareAccessLogs]; + if (where?.shareId) { + logs = logs.filter(l => l.shareId === where.shareId); + } + if (where?.userId) { + logs = logs.filter(l => l.userId === where.userId); + } + if (orderBy?.createdAt === 'desc') { + logs.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime()); + } + if (take) { + logs = logs.slice(0, take); + } + return Promise.resolve(logs); + }), + + count: jest.fn().mockImplementation(({ where }) => { + let logs = [...this.shareAccessLogs]; + if (where?.userId) { + logs = logs.filter(l => l.userId === where.userId); + } + if (where?.action) { + logs = logs.filter(l => l.action === where.action); + } + if (where?.createdAt?.gte) { + logs = logs.filter(l => l.createdAt >= where.createdAt.gte); + } + return Promise.resolve(logs.length); + }), + + deleteMany: jest.fn().mockImplementation(() => { + const count = this.shareAccessLogs.length; + this.shareAccessLogs = []; + return Promise.resolve({ count }); + }), + }; + + $connect = jest.fn().mockResolvedValue(undefined); + $disconnect = jest.fn().mockResolvedValue(undefined); + $queryRaw = jest.fn().mockResolvedValue([{ '?column?': 1 }]); + + // Helper methods for testing + reset(): void { + this.backupShares = []; + this.shareAccessLogs = []; + this.shareIdCounter = 1n; + this.logIdCounter = 1n; + jest.clearAllMocks(); + } + + getBackupShares(): MockBackupShare[] { + return [...this.backupShares]; + } + + getShareAccessLogs(): MockShareAccessLog[] { + return [...this.shareAccessLogs]; + } +} diff --git a/backend/services/backup-service/test/utils/test-utils.ts b/backend/services/backup-service/test/utils/test-utils.ts new file mode 100644 index 00000000..5d1dccd6 --- /dev/null +++ b/backend/services/backup-service/test/utils/test-utils.ts @@ -0,0 +1,90 @@ +import * as jwt from 'jsonwebtoken'; + +export const TEST_SERVICE_JWT_SECRET = 'test-super-secret-service-jwt-key-for-e2e-testing'; +export const TEST_ENCRYPTION_KEY = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef'; +export const TEST_ENCRYPTION_KEY_ID = 'test-key-v1'; + +export function generateServiceToken( + service: string = 'identity-service', + secret: string = TEST_SERVICE_JWT_SECRET, + expiresIn: string = '1h', +): string { + return jwt.sign( + { service, iat: Math.floor(Date.now() / 1000) }, + secret, + { expiresIn }, + ); +} + +export function generateExpiredServiceToken( + service: string = 'identity-service', + secret: string = TEST_SERVICE_JWT_SECRET, +): string { + return jwt.sign( + { service, iat: Math.floor(Date.now() / 1000) - 7200, exp: Math.floor(Date.now() / 1000) - 3600 }, + secret, + ); +} + +export function generatePublicKey(prefix: string = 'a'): string { + return '02' + prefix.repeat(64); +} + +export function createStoreSharePayload(overrides: Partial<{ + userId: string; + accountSequence: number; + publicKey: string; + encryptedShareData: string; + threshold: number; + totalParties: number; +}> = {}) { + return { + userId: overrides.userId ?? '12345', + accountSequence: overrides.accountSequence ?? 1001, + publicKey: overrides.publicKey ?? generatePublicKey('a'), + encryptedShareData: overrides.encryptedShareData ?? 'encrypted-share-data-base64', + threshold: overrides.threshold, + totalParties: overrides.totalParties, + }; +} + +export function createRetrieveSharePayload(overrides: Partial<{ + userId: string; + publicKey: string; + recoveryToken: string; + deviceId: string; +}> = {}) { + return { + userId: overrides.userId ?? '12345', + publicKey: overrides.publicKey ?? generatePublicKey('a'), + recoveryToken: overrides.recoveryToken ?? 'valid-recovery-token', + deviceId: overrides.deviceId, + }; +} + +export function createRevokeSharePayload(overrides: Partial<{ + userId: string; + publicKey: string; + reason: string; +}> = {}) { + return { + userId: overrides.userId ?? '12345', + publicKey: overrides.publicKey ?? generatePublicKey('a'), + reason: overrides.reason ?? 'ROTATION', + }; +} + +export const testEnvConfig = { + APP_ENV: 'test', + SERVICE_JWT_SECRET: TEST_SERVICE_JWT_SECRET, + BACKUP_ENCRYPTION_KEY: TEST_ENCRYPTION_KEY, + BACKUP_ENCRYPTION_KEY_ID: TEST_ENCRYPTION_KEY_ID, + MAX_RETRIEVE_PER_DAY: '3', + ALLOWED_SERVICES: 'identity-service,recovery-service', +}; + +export function setupTestEnv(): void { + Object.entries(testEnvConfig).forEach(([key, value]) => { + process.env[key] = value; + }); +} diff --git a/backend/services/backup-service/tsconfig.build.json b/backend/services/backup-service/tsconfig.build.json new file mode 100644 index 00000000..64f86c6b --- /dev/null +++ b/backend/services/backup-service/tsconfig.build.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] +} diff --git a/backend/services/backup-service/tsconfig.json b/backend/services/backup-service/tsconfig.json new file mode 100644 index 00000000..aba29b0e --- /dev/null +++ b/backend/services/backup-service/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "module": "nodenext", + "moduleResolution": "nodenext", + "resolvePackageJsonExports": true, + "esModuleInterop": true, + "isolatedModules": true, + "declaration": true, + "removeComments": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "allowSyntheticDefaultImports": true, + "target": "ES2023", + "sourceMap": true, + "outDir": "./dist", + "baseUrl": "./", + "incremental": true, + "skipLibCheck": true, + "strictNullChecks": true, + "forceConsistentCasingInFileNames": true, + "noImplicitAny": false, + "strictBindCallApply": false, + "noFallthroughCasesInSwitch": false + } +}