From 34ddbb9062f55b14a702bfde63375c2638a1a77c Mon Sep 17 00:00:00 2001 From: Christopher Ocares Date: Mon, 13 Apr 2026 08:31:19 -0500 Subject: [PATCH] entregable christopher ocares --- .dockerignore | 3 + .env.example | 36 + .gitignore | 27 + ADR.md | 434 + Dockerfile.base | 11 + README.md | 425 +- apps/fx-service/Dockerfile | 9 + apps/fx-service/nest-cli.json | 9 + apps/fx-service/package.json | 26 + apps/fx-service/src/app.module.ts | 28 + .../src/consumers/settle-fx.consumer.ts | 60 + apps/fx-service/src/fx/fx-rate.provider.ts | 44 + apps/fx-service/src/fx/fx.module.ts | 9 + apps/fx-service/src/fx/fx.service.ts | 51 + apps/fx-service/src/main.ts | 29 + apps/fx-service/test/fx.service.spec.ts | 76 + apps/fx-service/tsconfig.json | 8 + apps/fx-service/webpack.config.js | 20 + apps/orchestrator/Dockerfile | 10 + apps/orchestrator/nest-cli.json | 9 + apps/orchestrator/package.json | 30 + apps/orchestrator/src/app.module.ts | 41 + .../src/handlers/credit-failed.handler.ts | 15 + .../src/handlers/credit-succeeded.handler.ts | 15 + .../src/handlers/debit-failed.handler.ts | 15 + .../src/handlers/debit-succeeded.handler.ts | 15 + .../src/handlers/fx-ambiguous.handler.ts | 15 + .../src/handlers/fx-failed.handler.ts | 15 + .../src/handlers/fx-settled.handler.ts | 15 + .../src/handlers/receipt-emitted.handler.ts | 15 + apps/orchestrator/src/main.ts | 35 + .../src/saga/transfer-saga.entity.ts | 48 + .../src/saga/transfer-saga.module.ts | 32 + .../src/saga/transfer-saga.orchestrator.ts | 239 + .../src/saga/transfer-saga.repository.ts | 51 + .../src/saga/transfer-saga.states.ts | 10 + .../src/transfer/transfer.controller.ts | 14 + .../orchestrator/src/transfer/transfer.dto.ts | 18 + .../src/transfer/transfer.module.ts | 11 + .../src/transfer/transfer.service.ts | 43 + .../test/transfer-saga.orchestrator.spec.ts | 177 + apps/orchestrator/tsconfig.json | 8 + apps/orchestrator/webpack.config.js | 20 + apps/query-service/Dockerfile | 10 + apps/query-service/nest-cli.json | 9 + apps/query-service/package.json | 30 + apps/query-service/src/app.module.ts | 41 + apps/query-service/src/main.ts | 32 + .../src/projections/projection.module.ts | 9 + .../transfer-projection.handler.ts | 49 + .../src/query/transfer-query.controller.ts | 13 + .../src/query/transfer-query.module.ts | 11 + .../src/query/transfer-query.service.ts | 54 + .../read-model/transfer-read-model.entity.ts | 55 + .../read-model/transfer-read-model.module.ts | 11 + .../transfer-read-model.repository.ts | 20 + apps/query-service/tsconfig.json | 8 + apps/query-service/webpack.config.js | 20 + apps/receipt-service/Dockerfile | 9 + apps/receipt-service/nest-cli.json | 9 + apps/receipt-service/package.json | 29 + apps/receipt-service/src/app.module.ts | 40 + .../src/consumers/emit-receipt.consumer.ts | 40 + apps/receipt-service/src/main.ts | 29 + .../src/receipt/receipt.entity.ts | 33 + .../src/receipt/receipt.module.ts | 11 + .../src/receipt/receipt.service.ts | 45 + apps/receipt-service/tsconfig.json | 8 + apps/receipt-service/webpack.config.js | 20 + apps/wallet-service/Dockerfile | 9 + apps/wallet-service/nest-cli.json | 9 + apps/wallet-service/package.json | 29 + apps/wallet-service/src/app.module.ts | 45 + .../src/consumers/credit-wallet.consumer.ts | 40 + .../src/consumers/debit-wallet.consumer.ts | 40 + .../src/consumers/reverse-debit.consumer.ts | 28 + apps/wallet-service/src/main.ts | 29 + .../src/producers/wallet-event.producer.ts | 35 + .../src/wallet/wallet-operation.entity.ts | 25 + .../src/wallet/wallet.entity.ts | 29 + .../src/wallet/wallet.module.ts | 13 + .../src/wallet/wallet.repository.ts | 37 + .../src/wallet/wallet.service.ts | 79 + .../test/wallet.service.spec.ts | 151 + apps/wallet-service/tsconfig.json | 8 + apps/wallet-service/webpack.config.js | 20 + docker-compose.yml | 223 + jest.config.js | 17 + libs/common/filters/kafka-exception.filter.ts | 17 + .../interceptors/logging.interceptor.ts | 23 + .../interfaces/compensable.interface.ts | 4 + libs/common/interfaces/saga-step.interface.ts | 4 + .../commands/credit-wallet.command.ts | 6 + .../commands/debit-wallet.command.ts | 6 + .../commands/emit-receipt.command.ts | 8 + libs/contracts/commands/index.ts | 5 + .../commands/reverse-debit.command.ts | 7 + libs/contracts/commands/settle-fx.command.ts | 6 + libs/contracts/events/credit-failed.event.ts | 5 + .../events/credit-succeeded.event.ts | 6 + libs/contracts/events/debit-failed.event.ts | 5 + .../contracts/events/debit-succeeded.event.ts | 6 + libs/contracts/events/fx-ambiguous.event.ts | 7 + libs/contracts/events/fx-failed.event.ts | 4 + libs/contracts/events/fx-settled.event.ts | 7 + libs/contracts/events/index.ts | 9 + .../contracts/events/receipt-emitted.event.ts | 5 + .../events/transfer-updated.event.ts | 11 + libs/contracts/topics.ts | 22 + libs/helpers/idempotency-key.helper.ts | 4 + libs/helpers/money.helper.spec.ts | 65 + libs/helpers/money.helper.ts | 25 + libs/helpers/saga-id.helper.ts | 5 + package-lock.json | 8518 +++++++++++++++++ package.json | 39 + scripts/seed.sql | 18 + tsconfig.json | 26 + 117 files changed, 12483 insertions(+), 122 deletions(-) create mode 100644 .dockerignore create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 ADR.md create mode 100644 Dockerfile.base create mode 100644 apps/fx-service/Dockerfile create mode 100644 apps/fx-service/nest-cli.json create mode 100644 apps/fx-service/package.json create mode 100644 apps/fx-service/src/app.module.ts create mode 100644 apps/fx-service/src/consumers/settle-fx.consumer.ts create mode 100644 apps/fx-service/src/fx/fx-rate.provider.ts create mode 100644 apps/fx-service/src/fx/fx.module.ts create mode 100644 apps/fx-service/src/fx/fx.service.ts create mode 100644 apps/fx-service/src/main.ts create mode 100644 apps/fx-service/test/fx.service.spec.ts create mode 100644 apps/fx-service/tsconfig.json create mode 100644 apps/fx-service/webpack.config.js create mode 100644 apps/orchestrator/Dockerfile create mode 100644 apps/orchestrator/nest-cli.json create mode 100644 apps/orchestrator/package.json create mode 100644 apps/orchestrator/src/app.module.ts create mode 100644 apps/orchestrator/src/handlers/credit-failed.handler.ts create mode 100644 apps/orchestrator/src/handlers/credit-succeeded.handler.ts create mode 100644 apps/orchestrator/src/handlers/debit-failed.handler.ts create mode 100644 apps/orchestrator/src/handlers/debit-succeeded.handler.ts create mode 100644 apps/orchestrator/src/handlers/fx-ambiguous.handler.ts create mode 100644 apps/orchestrator/src/handlers/fx-failed.handler.ts create mode 100644 apps/orchestrator/src/handlers/fx-settled.handler.ts create mode 100644 apps/orchestrator/src/handlers/receipt-emitted.handler.ts create mode 100644 apps/orchestrator/src/main.ts create mode 100644 apps/orchestrator/src/saga/transfer-saga.entity.ts create mode 100644 apps/orchestrator/src/saga/transfer-saga.module.ts create mode 100644 apps/orchestrator/src/saga/transfer-saga.orchestrator.ts create mode 100644 apps/orchestrator/src/saga/transfer-saga.repository.ts create mode 100644 apps/orchestrator/src/saga/transfer-saga.states.ts create mode 100644 apps/orchestrator/src/transfer/transfer.controller.ts create mode 100644 apps/orchestrator/src/transfer/transfer.dto.ts create mode 100644 apps/orchestrator/src/transfer/transfer.module.ts create mode 100644 apps/orchestrator/src/transfer/transfer.service.ts create mode 100644 apps/orchestrator/test/transfer-saga.orchestrator.spec.ts create mode 100644 apps/orchestrator/tsconfig.json create mode 100644 apps/orchestrator/webpack.config.js create mode 100644 apps/query-service/Dockerfile create mode 100644 apps/query-service/nest-cli.json create mode 100644 apps/query-service/package.json create mode 100644 apps/query-service/src/app.module.ts create mode 100644 apps/query-service/src/main.ts create mode 100644 apps/query-service/src/projections/projection.module.ts create mode 100644 apps/query-service/src/projections/transfer-projection.handler.ts create mode 100644 apps/query-service/src/query/transfer-query.controller.ts create mode 100644 apps/query-service/src/query/transfer-query.module.ts create mode 100644 apps/query-service/src/query/transfer-query.service.ts create mode 100644 apps/query-service/src/read-model/transfer-read-model.entity.ts create mode 100644 apps/query-service/src/read-model/transfer-read-model.module.ts create mode 100644 apps/query-service/src/read-model/transfer-read-model.repository.ts create mode 100644 apps/query-service/tsconfig.json create mode 100644 apps/query-service/webpack.config.js create mode 100644 apps/receipt-service/Dockerfile create mode 100644 apps/receipt-service/nest-cli.json create mode 100644 apps/receipt-service/package.json create mode 100644 apps/receipt-service/src/app.module.ts create mode 100644 apps/receipt-service/src/consumers/emit-receipt.consumer.ts create mode 100644 apps/receipt-service/src/main.ts create mode 100644 apps/receipt-service/src/receipt/receipt.entity.ts create mode 100644 apps/receipt-service/src/receipt/receipt.module.ts create mode 100644 apps/receipt-service/src/receipt/receipt.service.ts create mode 100644 apps/receipt-service/tsconfig.json create mode 100644 apps/receipt-service/webpack.config.js create mode 100644 apps/wallet-service/Dockerfile create mode 100644 apps/wallet-service/nest-cli.json create mode 100644 apps/wallet-service/package.json create mode 100644 apps/wallet-service/src/app.module.ts create mode 100644 apps/wallet-service/src/consumers/credit-wallet.consumer.ts create mode 100644 apps/wallet-service/src/consumers/debit-wallet.consumer.ts create mode 100644 apps/wallet-service/src/consumers/reverse-debit.consumer.ts create mode 100644 apps/wallet-service/src/main.ts create mode 100644 apps/wallet-service/src/producers/wallet-event.producer.ts create mode 100644 apps/wallet-service/src/wallet/wallet-operation.entity.ts create mode 100644 apps/wallet-service/src/wallet/wallet.entity.ts create mode 100644 apps/wallet-service/src/wallet/wallet.module.ts create mode 100644 apps/wallet-service/src/wallet/wallet.repository.ts create mode 100644 apps/wallet-service/src/wallet/wallet.service.ts create mode 100644 apps/wallet-service/test/wallet.service.spec.ts create mode 100644 apps/wallet-service/tsconfig.json create mode 100644 apps/wallet-service/webpack.config.js create mode 100644 docker-compose.yml create mode 100644 jest.config.js create mode 100644 libs/common/filters/kafka-exception.filter.ts create mode 100644 libs/common/interceptors/logging.interceptor.ts create mode 100644 libs/common/interfaces/compensable.interface.ts create mode 100644 libs/common/interfaces/saga-step.interface.ts create mode 100644 libs/contracts/commands/credit-wallet.command.ts create mode 100644 libs/contracts/commands/debit-wallet.command.ts create mode 100644 libs/contracts/commands/emit-receipt.command.ts create mode 100644 libs/contracts/commands/index.ts create mode 100644 libs/contracts/commands/reverse-debit.command.ts create mode 100644 libs/contracts/commands/settle-fx.command.ts create mode 100644 libs/contracts/events/credit-failed.event.ts create mode 100644 libs/contracts/events/credit-succeeded.event.ts create mode 100644 libs/contracts/events/debit-failed.event.ts create mode 100644 libs/contracts/events/debit-succeeded.event.ts create mode 100644 libs/contracts/events/fx-ambiguous.event.ts create mode 100644 libs/contracts/events/fx-failed.event.ts create mode 100644 libs/contracts/events/fx-settled.event.ts create mode 100644 libs/contracts/events/index.ts create mode 100644 libs/contracts/events/receipt-emitted.event.ts create mode 100644 libs/contracts/events/transfer-updated.event.ts create mode 100644 libs/contracts/topics.ts create mode 100644 libs/helpers/idempotency-key.helper.ts create mode 100644 libs/helpers/money.helper.spec.ts create mode 100644 libs/helpers/money.helper.ts create mode 100644 libs/helpers/saga-id.helper.ts create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 scripts/seed.sql create mode 100644 tsconfig.json diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..d6082b3c --- /dev/null +++ b/.dockerignore @@ -0,0 +1,3 @@ +**/dist +**/node_modules +**/.git diff --git a/.env.example b/.env.example new file mode 100644 index 00000000..ad76e568 --- /dev/null +++ b/.env.example @@ -0,0 +1,36 @@ +# Kafka +KAFKA_BROKERS=localhost:9092 +KAFKA_CLIENT_ID=wallet-transfer-saga +KAFKA_CONSUMER_GROUP_ORCHESTRATOR=orchestrator-group +KAFKA_CONSUMER_GROUP_WALLET=wallet-service-group +KAFKA_CONSUMER_GROUP_FX=fx-service-group +KAFKA_CONSUMER_GROUP_RECEIPT=receipt-service-group +KAFKA_CONSUMER_GROUP_QUERY=query-service-group + +# Kafka interna (inter-broker dentro de docker-compose) +KAFKA_INTERNAL_PORT=29092 +KAFKA_EXTERNAL_PORT=9092 +ZOOKEEPER_PORT=2181 + +# PostgreSQL +POSTGRES_HOST=localhost +POSTGRES_PORT=5432 +POSTGRES_USER=saga_user +POSTGRES_PASSWORD=saga_pass +POSTGRES_DB=wallet_saga + +# Servicios — puertos HTTP +ORCHESTRATOR_PORT=3001 +QUERY_SERVICE_PORT=3005 + +# FX mock +FX_MOCK_PORT=4000 +FX_MOCK_DEFAULT_DELAY_MS=300 +FX_MOCK_DEFAULT_RATE=3.72 + +# FX service — timeout en ms para considerar estado ambiguo +FX_API_URL=http://fx-mock:4000 +FX_TIMEOUT_MS=5000 + +# Money — unidad mínima (centavos). Cambiar solo si la moneda usa otra subdivisión. +MONEY_SUBUNIT_FACTOR=100 diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..827d7435 --- /dev/null +++ b/.gitignore @@ -0,0 +1,27 @@ +# Dependencies +node_modules/ + +# Build output +dist/ +**/dist/ + +# Environment files — nunca subir secrets reales +.env +.env.local +.env.production + +# Logs +*.log +npm-debug.log* + +# OS +.DS_Store +Thumbs.db + +# IDE +.vscode/ +.idea/ + +# Jest cache +coverage/ +.jest-cache/ diff --git a/ADR.md b/ADR.md new file mode 100644 index 00000000..8955ee8d --- /dev/null +++ b/ADR.md @@ -0,0 +1,434 @@ +# Architecture Decision Record — Wallet Transfer Distributed Saga + +**Challenge:** Challenge 2 — Yape Code Challenge (Tech Lead / Staff Level) +**Date:** 2026-04-12 +**Status:** Accepted + +--- + +## ADR-001: Orquestación sobre Coreografía + +### Decisión +La saga se implementa como un **orquestador centralizado** (`TransferSagaOrchestrator`) que dirige el flujo completo emitiendo comandos y reaccionando a eventos de dominio. + +### Alternativas consideradas + +| Enfoque | Trade-offs | +|---------|-----------| +| **Coreografía** | Cada servicio reacciona a eventos y emite el siguiente. Sin coordinador central. El estado total de la saga es implícito — para saber si una transferencia está en curso hay que correlacionar logs de 5 servicios. La compensación se vuelve un concern distribuido: ¿quién decide cuándo hacer reverse-debit? | +| **Orquestación (elegida)** | Estado de saga explícito y persistido. Compensación centralizada. Trade-off: el orquestador es un SPOF — mitigado con estado en PostgreSQL, por lo que puede recuperarse de cualquier crash. | + +### Razonamiento +El reto pide explícitamente "compensation logic for failures." Con orquestación, el lugar donde vive esa lógica es inequívoco: + +```typescript +// apps/orchestrator/src/saga/transfer-saga.orchestrator.ts +async onCreditFailed(event: CreditFailedEvent): Promise { + const saga = await this.loadSaga(event.sagaId); + if (!saga) return; + + const transitioned = await this.sagaRepo.transitionState( + saga.id, SagaState.DEBITED, SagaState.COMPENSATING, saga.version, + { failureReason: event.reason }, + ); + if (!transitioned) return; + + await this.publishUpdate({ ...saga, state: SagaState.COMPENSATING, failureReason: event.reason }); + await this.emitReverseDebit(saga, event.reason); +} +``` + +Con coreografía, un evento `credit.failed` tendría que llegar al `wallet-service` con suficiente contexto para revertir el débito — creando acoplamiento implícito entre servicios que no deberían conocerse entre sí. + +--- + +## ADR-002: State Machine hand-rolled sobre Temporal + +### Decisión +La máquina de estados está implementada directamente en `TransferSagaOrchestrator` con un enum `SagaState` y transiciones atómicas mediante optimistic locking en PostgreSQL. No se usa ningún workflow engine. + +### Alternativas consideradas + +| Enfoque | Trade-offs | +|---------|-----------| +| **Temporal** | Ejecución durable de workflows out of the box. Maneja retries, timeouts e historial. Requiere servidor Temporal — dependencia operacional adicional no en el stack del challenge. | +| **Conductor / Camunda** | Plataformas maduras con UI. Pesadas para un challenge enfocado. | +| **State machine hand-rolled (elegida)** | Control total. Cero infraestructura extra. Las transiciones son código testeable en aislamiento. | + +### Razonamiento +El reto evalúa "distributed systems reasoning", no "usar el tool correcto". Una state machine hand-rolled demuestra entendimiento de los fundamentos del saga pattern — transiciones idempotentes, prevención de double-processing, compensación explícita — de forma que un workflow de Temporal abstraería. + +La clave está en `transitionState()`: + +```typescript +// apps/orchestrator/src/saga/transfer-saga.repository.ts +async transitionState( + id: string, + expectedState: SagaState, + nextState: SagaState, + version: number, + extra: Partial = {}, +): Promise { + const result = await this.repo + .createQueryBuilder() + .update(TransferSagaEntity) + .set({ state: nextState, ...extra }) + .where('id = :id AND state = :expectedState AND version = :version', { + id, + expectedState, + version, + }) + .execute(); + + return (result.affected ?? 0) > 0; +} +``` + +El `WHERE state = :expectedState AND version = :version` convierte cada transición en una operación CAS (compare-and-swap). Si dos eventos del mismo saga llegan simultáneamente al orquestador, solo uno ejecutará el `UPDATE` con `affected = 1`. El segundo encontrará `affected = 0` y abortará sin emitir el siguiente comando. Esto previene que una saga se ejecute dos veces en paralelo sin requerir locks distribuidos. + +--- + +## ADR-003: PostgreSQL como base de datos + +### Decisión +PostgreSQL 17 para toda la persistencia: estado de saga, balances de wallets, audit log de operaciones, recibos, y el read model CQRS. + +### Alternativas consideradas + +| Enfoque | Trade-offs | +|---------|-----------| +| **MongoDB** | Document model encaja bien con payloads de saga. Sin `SELECT FOR UPDATE` nativo. Balance negativo requiere locking a nivel de aplicación. | +| **Redis** | Rápido, natural para saga state. No suficientemente durable para operaciones financieras sin AOF persistence + fsync. Sin `SELECT FOR UPDATE`. | +| **PostgreSQL (elegida)** | ACID por servicio. `SELECT FOR UPDATE` nativo para concurrencia de wallets. Única instancia para el challenge — en producción cada servicio tendría su propia DB. | + +### Razonamiento +El reto exige "concurrent safety (prevent negative balances)." `SELECT FOR UPDATE` es la solución canónica: el wallet-service adquiere un row-level lock, verifica el balance, aplica la operación, y libera — todo en una sola transacción. + +```typescript +// apps/wallet-service/src/wallet/wallet.repository.ts +async findByIdForUpdate( + walletId: string, + manager: EntityManager, +): Promise { + return manager + .getRepository(WalletEntity) + .createQueryBuilder('wallet') + .setLock('pessimistic_write') // → SELECT ... FOR UPDATE + .where('wallet.id = :walletId', { walletId }) + .getOne(); +} +``` + +Toda la operación ocurre dentro de una transacción local. La idempotencia se verifica antes del lock: si la `idempotency_key` ya existe en `wallet_operations`, la operación se descarta sin tocar el balance. Solo si es nueva se adquiere el lock y se muta el estado. + +```typescript +// apps/wallet-service/src/wallet/wallet.service.ts +private async executeOperation( + idempotencyKey: string, + params: WalletOperationParams, + type: OperationType, +): Promise { + await this.dataSource.transaction(async (manager) => { + // 1. Idempotency check — si ya procesamos, salir sin tocar balance + const alreadyProcessed = await manager.findOneBy(WalletOperationEntity, { idempotencyKey }); + if (alreadyProcessed) return; + + // 2. Lock pesimista — bloquea la fila hasta el commit + const wallet = await this.walletRepo.findByIdForUpdate(params.walletId, manager); + if (!wallet) throw new Error(`Wallet ${params.walletId} not found`); + + const currentBalance = BigInt(wallet.balance); + + // 3. subtractMoney lanza INSUFFICIENT_FUNDS si b > a — nunca balance negativo + if (type === OperationType.DEBIT) { + wallet.balance = subtractMoney(currentBalance, params.amount).toString(); + } else { + wallet.balance = addMoney(currentBalance, params.amount).toString(); + } + + await this.walletRepo.saveWithManager(wallet, manager); + await manager.save(WalletOperationEntity, operation); + }); +} +``` + +--- + +## ADR-004: CQRS Read Model vía evento `transfer.updated` + +### Decisión +El orquestador emite un evento `transfer.updated` en **cada transición de estado**, con el contexto completo del saga. El query-service se suscribe exclusivamente a este tópico y hace upsert del read model en cada evento. + +### Alternativas consideradas + +| Enfoque | Trade-offs | +|---------|-----------| +| **Query-service suscrito a todos los eventos de dominio** | Necesita reconstruir el estado a partir de `debit.succeeded`, `fx.settled`, etc. El query-service debe conocer lógica de negocio. Cualquier nuevo evento de dominio requiere actualizar el query-service. | +| **Lectura directa a la DB del orquestador** | Rompe CQRS. Acoplamiento fuerte entre read y write sides. | +| **`transfer.updated` como evento de proyección (elegida)** | El orquestador es la autoridad del estado. Emite un evento auto-contenido tras cada transición. El query-service es un projector tonto — sin lógica de negocio, solo persistencia. | + +### Razonamiento +El orquestador ya conoce el estado completo del saga en cada transición. Emitir ese estado como evento de proyección es cero costo adicional: + +```typescript +// apps/orchestrator/src/saga/transfer-saga.orchestrator.ts +private async publishUpdate( + saga: Pick, + receiptId: string | null = null, +): Promise { + const event: TransferUpdatedEvent = { + sagaId: saga.id, + state: saga.state, + debitWalletId: saga.debitWalletId, + creditWalletId: saga.creditWalletId, + amount: saga.amount, + currency: saga.currency, + fxRate: saga.fxRate, + receiptId, + failureReason: saga.failureReason, + }; + await firstValueFrom(this.kafka.emit(KafkaTopic.TRANSFER_UPDATED, event)); +} +``` + +### Garantía de consistencia +El read model es **eventually consistent**. La ventana de inconsistencia es el lag del consumer group del query-service. En condiciones normales < 500ms. El response incluye metadata que documenta esto explícitamente: + +```typescript +// apps/query-service/src/query/transfer-query.service.ts +_consistency: { + // El read model se actualiza vía proyección de eventos Kafka. + // La ventana de inconsistencia es el lag del consumer group query-service-group. + // En condiciones normales < 500ms. Monitorear con kafka.consumer.lag en métricas. + model: 'eventual', + eventVersion: model.eventVersion, // incrementa con cada evento proyectado +}, +``` + +`eventVersion` permite a los clientes detectar si están leyendo un snapshot desactualizado comparando contra el valor esperado. + +--- + +## ADR-005: Tipos wire — `string` para cantidades monetarias + +### Decisión +Todas las cantidades monetarias en mensajes Kafka usan `amount: string` (enteros en centavos), no `number` ni `bigint`. + +### Alternativas consideradas + +| Tipo | Trade-offs | +|------|-----------| +| **`number`** | IEEE 754. `0.1 + 0.2 !== 0.3`. Inaceptable para finanzas. | +| **`bigint`** | Aritmética entera precisa. No JSON-serializable — `JSON.stringify` lanza `TypeError: Do not know how to serialize a BigInt`. Los mensajes Kafka son JSON. | +| **`string` (elegida)** | JSON-safe. Sin pérdida de precisión. `"10000"` representa 100.00 PEN en centavos. | + +### Razonamiento +La aritmética financiera ocurre **dentro de los servicios** con `BigInt`, usando `money.helper.ts`. El wire format es `string` exclusivamente por compatibilidad de serialización: + +```typescript +// libs/helpers/money.helper.ts +export function subtractMoney(a: bigint, b: bigint): bigint { + if (b > a) throw new Error('INSUFFICIENT_FUNDS'); + return a - b; +} + +export function addMoney(a: bigint, b: bigint): bigint { + return a + b; +} +``` + +En el wallet-service, la conversión es explícita en ambas direcciones: + +```typescript +// Deserialización: string → bigint para aritmética +const currentBalance = BigInt(wallet.balance); +wallet.balance = subtractMoney(currentBalance, params.amount).toString(); + +// Serialización: bigint → string para persistencia y wire +operation.amount = params.amount.toString(); +``` + +Este patrón sigue el approach de Stripe y otros sistemas financieros de referencia. + +--- + +## ADR-006: Estado `AMBIGUOUS` para FX timeout (Escalation Pattern) + +### Decisión +Cuando la API de FX no responde dentro del timeout configurado, la saga transiciona a `AMBIGUOUS`. No se dispara compensación automática. + +### Alternativas consideradas + +| Enfoque | Trade-offs | +|---------|-----------| +| **Marcar como `FAILED` + compensar** | Sencillo. Peligroso: si el FX sí procesó el settlement y revertimos el débito, creamos una inconsistencia financiera real — el dinero existe en FX pero el wallet-service lo devolvió. | +| **Reintentar indefinidamente** | Evita tomar una decisión sin información. Bloquea el saga en un loop; un proveedor FX caído puede dejar miles de sagas bloqueados consumiendo recursos. | +| **Timeout corto + `FAILED` automático** | Elimina la ambigüedad con una decisión arbitraria. El riesgo financiero sigue siendo el mismo que la primera alternativa. | +| **Estado `AMBIGUOUS` + reconciliación manual (elegida)** | Honesto sobre la incertidumbre. No crea inconsistencias financieras. Requiere un proceso externo para resolución, pero ese proceso puede actuar con información completa del proveedor FX. | + +### Razonamiento +Si el llamado FX hace timeout, no sabemos si el proveedor procesó el settlement o no: +- Si revertimos el débito y el FX sí se procesó → inconsistencia financiera +- Si marcamos como `FAILED` y el FX sí se procesó → el dinero está en el limbo + +`AMBIGUOUS` es el único estado sin ruta de compensación automática: + +```typescript +// apps/orchestrator/src/saga/transfer-saga.orchestrator.ts +async onFxAmbiguous(event: FxAmbiguousEvent): Promise { + const saga = await this.loadSaga(event.sagaId); + if (!saga) return; + + // No se compensa — el estado real del FX es desconocido. + // Un job de reconciliación externo debe resolverlo antes de reintentar o revertir. + const failureReason = `FX timeout after ${event.timeoutMs}ms`; + await this.sagaRepo.transitionState( + saga.id, SagaState.CREDITED, SagaState.AMBIGUOUS, saga.version, + { failureReason }, + ); + await this.publishUpdate({ ...saga, state: SagaState.AMBIGUOUS, failureReason }); + this.logger.error(`Saga ${saga.id} AMBIGUOUS — manual reconciliation required`); +} +``` + +Un proceso de reconciliación externo (fuera del scope del challenge) re-consultaría al proveedor FX y resolvería el saga completándolo o compensándolo. + +--- + +## ADR-007: Setup del Kafka producer — `producerOnlyMode` + conexión eager + +### Decisión +Todos los servicios que solo producen mensajes Kafka usan: +1. `producerOnlyMode: true` en `ClientsModule.register()` +2. `kafka.connect()` llamado eagerly en `onModuleInit()` +3. `await firstValueFrom(this.kafka.emit(...))` en cada publicación + +### Alternativas consideradas + +| Enfoque | Trade-offs | +|---------|-----------| +| **`emit()` sin `firstValueFrom()`** | Más simple visualmente. El Observable es lazy — sin suscriptor el mensaje se descarta silenciosamente. No hay error, no hay log, el saga avanza creyendo que emitió cuando no lo hizo. Fallo silencioso. | +| **`connect()` lazy (default de NestJS)** | El primer `emit()` dispara `connect()` que inicia producer + consumer group. El consumer join tarda 3+ segundos. El primer mensaje se envía con alta latencia y puede perderse si el timeout de la transacción HTTP es corto. | +| **Sin `producerOnlyMode`** | `connect()` crea un consumer group para reply topics aunque el servicio nunca los use. Dos consumer groups en Kafka por cada servicio que solo necesita producir, más latencia de startup. | +| **`producerOnlyMode: true` + eager connect + `firstValueFrom` (elegida)** | Conexión al broker en startup — sin latencia en el critical path. Solo producer, sin consumer groups innecesarios. `firstValueFrom()` garantiza entrega confirmada por el broker antes de continuar. | + +### Razonamiento +`ClientKafka.emit()` de NestJS retorna un `Observable` lazy — sin suscriptor, el mensaje nunca se envía. `firstValueFrom()` suscribe y resuelve cuando el broker confirma la recepción. + +Sin `producerOnlyMode: true`, `connect()` también arranca un consumer group para reply topics — innecesario para emitters fire-and-forget y añade 3+ segundos de latencia al primer emit mientras el consumer se une al grupo. + +```typescript +// apps/orchestrator/src/saga/transfer-saga.orchestrator.ts +async onModuleInit(): Promise { + // Conectar el producer antes de que llegue el primer request HTTP o mensaje Kafka. + // Con producerOnlyMode: true, connect() solo inicializa el producer — sin consumer group. + await this.kafka.connect(); +} + +private async emitDebit(saga: TransferSagaEntity): Promise { + const cmd: DebitWalletCommand = { + sagaId: saga.id, + walletId: saga.debitWalletId, + amount: saga.amount, + currency: saga.currency, + }; + // firstValueFrom() es necesario: emit() retorna un Observable lazy. + // Sin suscripción, el mensaje se descarta silenciosamente. + await firstValueFrom(this.kafka.emit(KafkaTopic.DEBIT_WALLET, cmd)); +} +``` + +--- + +## ADR-008: Webpack bundling para resolución de path aliases + +### Decisión +Cada servicio compila con webpack (`nest build --webpack`) en lugar de la transpilación estándar de tsc. + +### Problema +Los path aliases de TypeScript (`@contracts/`, `@common/`, `@helpers/`) son resueltos por el compilador durante el type checking pero **no son reescritos** en el JavaScript de salida. Node.js no puede resolver `@contracts/topics` en runtime. + +### Alternativas consideradas + +| Enfoque | Trade-offs | +|---------|-----------| +| **`tsconfig-paths/register`** | Resolución de paths en runtime. Añade overhead. Puede generar conflictos con distintos modos de resolución de módulos. | +| **`module-alias`** | Similar al anterior. Requiere mantener un segundo mapa de aliases fuera de `tsconfig.json`. | +| **Imports relativos en todo el código** | Elimina el problema. `../../libs/contracts/topics` es ilegible y frágil en un monorepo. | +| **Webpack + TsconfigPathsPlugin (elegida)** | Los aliases se reescriben en compile time. Cero overhead en runtime. El `dist/main.js` resultante no contiene ninguna referencia a `@contracts/` — están reemplazadas por el código real de los módulos. | + +### Configuración +Cada servicio tiene un `webpack.config.js` idéntico: + +```javascript +// apps/orchestrator/webpack.config.js +const nodeExternals = require('webpack-node-externals'); +const { TsconfigPathsPlugin } = require('tsconfig-paths-webpack-plugin'); +const path = require('path'); + +module.exports = { + entry: './src/main.ts', + target: 'node', + // nodeExternals mantiene node_modules externos (no bundleados) — + // evita el bundle de 200MB mientras sigue resolviendo los aliases de libs/ + externals: [nodeExternals({ modulesDir: path.resolve(__dirname, '../../node_modules') })], + resolve: { + extensions: ['.ts', '.js'], + // TsconfigPathsPlugin reescribe @contracts/ → ruta relativa real en compile time + plugins: [new TsconfigPathsPlugin({ configFile: './tsconfig.json' })], + }, + module: { + rules: [{ test: /\.ts$/, use: 'ts-loader', exclude: /node_modules/ }], + }, + output: { filename: 'main.js', path: path.resolve(__dirname, 'dist') }, +}; +``` + +--- + +## ADR-009: Graceful Shutdown en consumers Kafka + +### Decisión +Todos los servicios llaman `app.enableShutdownHooks()` en el bootstrap, antes de conectarse a Kafka. + +### Problema +Sin shutdown hooks, cuando Docker envía `SIGTERM` (deploy, restart, scale-down), el proceso termina inmediatamente con mensajes Kafka en vuelo. El consumer no hace commit del offset — Kafka reentrega esos mensajes al próximo consumer tras el rebalanceo. Si la transacción de DB ya hizo commit parcialmente en ese momento, hay inconsistencia entre el estado persistido y el offset de Kafka. + +### Lo que `enableShutdownHooks()` hace +NestJS intercepta `SIGTERM` y llama `app.close()`, que a su vez llama `onApplicationShutdown()` en todos los providers registrados. El `ServerKafka` de NestJS implementa este lifecycle hook para desconectar el consumer de forma limpia (commit de offsets pendientes, pausa de consumo de nuevas particiones). + +### Limitación conocida +`enableShutdownHooks()` da una ventana de shutdown ordenado pero no garantiza exactly-once processing. Si la transacción tarda más que el `terminationGracePeriodSeconds` de Kubernetes (default: 30s), el proceso igualmente se mata con `SIGKILL`. La solución completa requiere idempotencia en el consumer (que sí existe vía `idempotencyKey`) para que la reentrega sea inocua. + +--- + +## Qué cambiaría con más tiempo + +1. **Outbox pattern para produces Kafka** — Actualmente guardar el estado en DB y emitir a Kafka son dos operaciones separadas. Si el proceso crashea entre ellas, el estado de la saga se guarda pero el mensaje Kafka se pierde. Un outbox transaccional (escribir en DB + tabla outbox en una sola transacción, relay process lee el outbox) eliminaría este gap completamente. + +2. **Dead Letter Topic (DLT) por cada comando Kafka** — Cuando `KafkaExceptionFilter` relanza el error, KafkaJS reinicia el consumer y reintenta el mismo mensaje. Tras N reintentos, el consumer crashea indefinidamente. Un DLT capturaría esos mensajes envenenados, los sacaría del flujo principal, y los haría visibles para operaciones sin bloquear el resto del pipeline. + +3. **Correlation ID estructurado en logs** — El `sagaId` aparece en los logs del orchestrator pero no fluye como campo JSON estructurado a través de todos los servicios. En producción se implementaría con `AsyncLocalStorage` propagando el `sagaId` desde el mensaje Kafka al contexto de ejecución, y el `LoggingInterceptor` lo emitería en cada línea de log. + +4. **DB por servicio** — Todos los servicios comparten una instancia PostgreSQL por simplicidad. En producción, cada servicio debe poseer sus datos sin schema compartido. + +5. **Job de reconciliación para AMBIGUOUS** — Un proceso programado que re-consulta al proveedor FX y resuelve los sagas en estado `AMBIGUOUS` automáticamente. + +6. **Integration tests con testcontainers** — Los tests actuales son unitarios. Tests de integración con Postgres + Kafka reales en Docker darían garantías mucho más fuertes para el flujo multi-paso del saga. + +7. **Estrategia explícita de rebuild del read model** — Si el read model se corrompe o se agrega un campo nuevo, la solución con este diseño es truncar `transfer_read_models` y reprocesar `transfer.updated` desde offset 0. Esto funciona porque `transfer.updated` contiene el estado completo del saga en cada evento — no hay que reconstruir desde múltiples streams. Vale la pena documentarlo formalmente y exponer un endpoint de admin (`POST /admin/rebuild-projection`) para ejecutarlo de forma controlada. + +--- + +## Limitaciones conocidas + +| Limitación | Impacto | Mitigación | +|-----------|---------|-----------| +| Sin outbox transaccional | Estado puede guardarse sin que el mensaje Kafka correspondiente se envíe | Probabilidad baja; la idempotencia en consumers hace la reentrega inocua; el saga puede re-conducirse desde su estado persistido | +| Sin Dead Letter Topic | Mensajes envenenados reinician el consumer indefinidamente | `KafkaExceptionFilter` loguea el error — visible para operaciones, pero bloquea la partición hasta resolución manual | +| Correlation ID no fluye como campo estructurado | El `sagaId` aparece en logs del orchestrator pero no en los demás servicios | Todos los logs incluyen el nombre del servicio; el `sagaId` está en el payload del mensaje Kafka para correlación manual | +| Una partición Kafka por tópico | Sin escalado horizontal de consumers. Con múltiples instancias, los rebalanceos de particiones causarían reentrega masiva de mensajes en vuelo | Aceptable para una instancia por servicio; en producción: múltiples particiones + `group.instance.id` (static membership) para minimizar rebalanceos | +| Instancia PostgreSQL compartida | Servicios no totalmente aislados a nivel de datos | Cada servicio usa conexiones TypeORM separadas y conjuntos de entidades distintos | +| `AMBIGUOUS` requiere resolución manual | FX timeouts dejan la saga en limbo | Job de reconciliación documentado como próximo paso | +| Race condition en creación de saga | Dos POSTs simultáneos idénticos pueden crear dos sagas | El índice único en `idempotencyKey` atrapa el segundo con error de constraint; en producción: `INSERT ... ON CONFLICT DO NOTHING` | +| Sin autenticación en endpoints HTTP | `POST /transfers` y `GET /transfers/:id` son abiertos | Fuera del scope de un challenge de infraestructura distribuida | diff --git a/Dockerfile.base b/Dockerfile.base new file mode 100644 index 00000000..70f3e05d --- /dev/null +++ b/Dockerfile.base @@ -0,0 +1,11 @@ +FROM node:22-alpine AS base +WORKDIR /app +COPY package.json package-lock.json tsconfig.json ./ +COPY apps/orchestrator/package.json ./apps/orchestrator/ +COPY apps/wallet-service/package.json ./apps/wallet-service/ +COPY apps/fx-service/package.json ./apps/fx-service/ +COPY apps/receipt-service/package.json ./apps/receipt-service/ +COPY apps/query-service/package.json ./apps/query-service/ +RUN npm ci --workspaces --include-workspace-root +COPY libs/ ./libs/ +COPY apps/ ./apps/ diff --git a/README.md b/README.md index 14377fae..a942c264 100644 --- a/README.md +++ b/README.md @@ -1,197 +1,378 @@ -# Yape Code Challenge 🚀 +# Wallet Transfer — Distributed Saga -Welcome. This challenge is designed for experienced engineers being considered for **tech lead and staff-level** roles. It tests your ability to reason about distributed systems, event-driven architecture, and platform design — not just your ability to ship working code. - -There are three challenges. **Pick one.** Go deep rather than broad. +**Yape Code Challenge — Challenge 2** +Stack: NestJS · Kafka · PostgreSQL · Docker Compose --- -## Table of Contents +## El challenge + +Transferir fondos entre wallets de forma atómica sin transacciones distribuidas. La implementación debe ser segura para replay desde cualquier paso, compensar en caso de fallo, y servir un read model CQRS que refleje el estado actual de cada transferencia. + +**Decisiones clave:** +- **Orquestación** sobre coreografía — estado de saga explícito y centralizado +- **State machine hand-rolled** sobre Temporal — demuestra los fundamentos sin infraestructura extra +- **PostgreSQL** con `SELECT FOR UPDATE` para concurrencia de wallets — balance negativo imposible +- **`transfer.updated`** como evento de proyección — el query-service es un projector tonto sin lógica de negocio +- **`string`** para cantidades monetarias en wire format — `BigInt` no es JSON-serializable -- [What we're evaluating](#what-were-evaluating) -- [Challenge 1 — Payment settlement pipeline](#challenge-1--payment-settlement-pipeline) -- [Challenge 2 — Wallet transfer with distributed saga](#challenge-2--wallet-transfer-with-distributed-saga) -- [Challenge 3 — Shared platform library design](#challenge-3--shared-platform-library-design) -- [Tech stack](#tech-stack) -- [Submission](#submission) +El razonamiento completo de cada decisión y sus alternativas rechazadas está en [`ADR.md`](./ADR.md). --- -## What we're evaluating +## Arquitectura -We're not looking for a perfect system. We're looking for evidence that you think like a tech lead: +``` +POST /transfers + │ + ▼ + ┌─────────────┐ wallet.debit.cmd ┌───────────────┐ + │ Orchestrator│ ────────────────────────▶│ Wallet Service│ + │ (port 3001)│ ◀────────────────────── │ │ + └─────────────┘ wallet.debit.succeeded └───────────────┘ + │ + │ wallet.credit.cmd + ▼ + ┌───────────────┐ fx.settle.cmd ┌────────────┐ + │ Wallet Service│──────────────────────▶│ FX Service │ + └───────────────┘◀──────────────────── └────────────┘ + fx.settled │ + │ receipt.emit.cmd + ▼ + ┌─────────────────┐ + │ Receipt Service │ + └─────────────────┘ + │ receipt.emitted + ▼ + ┌─────────────────┐ + │ Orchestrator │ ──▶ transfer.updated (×5) + └─────────────────┘ + │ + ▼ + ┌──────────────────┐ + │ Query Service │ + │ (port 3005) │ + └──────────────────┘ + +GET /transfers/:sagaId ──────────────────────────────────▶ { status: "COMPLETED", ... } +``` -- You treat trade-offs as first-class decisions, not implementation details. -- You build for the engineer who reads your code six months from now, not for the PR reviewer today. -- You can identify the antipattern in a brief before someone points it out to you. -- You know what you deliberately left out — and why. +### Estados del saga -Every challenge includes an **optional escalation**. It's genuinely optional; finishing the core well beats rushing to the escalation. +``` +PENDING ──[debit ok]──▶ DEBITED ──[credit ok]──▶ CREDITED ──[fx ok]──▶ FX_SETTLED ──[receipt ok]──▶ COMPLETED + │ │ │ + │[debit fail] │[credit fail] │[fx fail] + ▼ ▼ ▼ +FAILED COMPENSATING COMPENSATING + │ │ + [reverse ok] [reverse ok] + ▼ ▼ + FAILED FAILED + + [fx timeout] + ▼ + AMBIGUOUS ← requiere reconciliación manual +``` --- -## Challenge 1 — Payment settlement pipeline - -### Premise +## Requisitos previos -You're building the payment processing backbone for a multi-country wallet. A payment initiated in one country may touch ledger entries, fraud scoring, and notification services that are fully independent. Your solution must remain correct under partial failures and message redelivery. +- Docker Desktop con Docker Compose v2 +- Node.js 22 LTS (solo para ejecutar tests — no necesario para levantar el sistema) +- Puerto 3001, 3005, 4000, 5432, 9092 libres -### Architecture overview +--- +## Levantar el sistema -``` -Payment API ──► Outbox table ──[relay]──► Kafka topic - (payment.created.v1) - │ - ┌────────────────────┼────────────────────┐ - ▼ ▼ ▼ - FraudConsumer LedgerConsumer NotifyConsumer - (risk scoring) (double-entry write) (push / email) - │ │ - └────────┬───────────┘ - ▼ - Status saga - (eventual consistency) - │ - ┌──────────┴──────────┐ - ▼ (on failure) ▼ (on success) - DLT topic payment.settled.v1 - (payment.failed.v1) -``` +```bash +# 1. Clonar el repositorio +git clone +cd wallet-transfer-saga -### Required deliverables +# 2. Construir la imagen base (instala todas las dependencias dentro de Docker) +# Solo necesario la primera vez o tras cambios en package.json +docker build -t wallet-transfer-saga-base -f Dockerfile.base . -1. **Transactional outbox** — A `PaymentService` (NestJS) that writes a payment record and its outbox entry in a single local transaction. A separate relay process publishes to Kafka. The broker must never be called inside the database transaction. +# 3. Construir los 5 servicios +docker compose build -2. **Idempotent consumers** — At least two downstream consumers (`FraudConsumer`, `LedgerConsumer`) in separate NestJS modules. Reprocessing the same event twice must produce no observable side effect. +# 4. Levantar todo (Kafka, PostgreSQL, fx-mock, 5 servicios) +docker compose up -d -3. **DLT handler** — When a consumer exceeds its retry budget, emit a compensating event to a Dead Letter Topic rather than silently dropping the message. +# Esperar ~30 segundos para que Kafka se estabilice y los topics se creen +# Los servicios tienen healthchecks — kafka-init crea los 14 topics antes de arrancar los servicios +# db-seed inserta las wallets de prueba tras que TypeORM cree las tablas +``` -4. **Status query endpoint** — A GET endpoint that reflects eventual consistency honestly. A payment may return `pending` after creation and only transition to `settled` or `failed` once both consumers have acknowledged. +Verificar que todos los servicios están corriendo: -### What a strong solution looks like +```bash +docker compose ps +``` -- The outbox relay is a distinct process boundary — not a `setInterval` in the same NestJS app. -- Idempotency keys live on the consumer side (keyed by `eventId`), not on the producer side. -- The status endpoint documents its consistency guarantees explicitly — either in code comments or in an API response envelope. -- The candidate can explain what happens if the relay crashes between writing the outbox entry and publishing to Kafka. +Debes ver los siguientes servicios con status `Up` o `Exited (0)`: + +| Servicio | Puerto | Notas | +|----------|--------|-------| +| `kafka` | 9092 | Broker principal | +| `zookeeper` | 2181 | Coordinador de Kafka | +| `postgres` | 5432 | Base de datos compartida | +| `fx-mock` | 4000 | API FX simulada (rate: 3.72, delay: 300ms) | +| `kafka-init` | — | Crea los 14 topics y termina (`Exited 0`) | +| `db-seed` | — | Inserta wallets de prueba y termina (`Exited 0`) | +| `orchestrator` | 3001 | State machine + HTTP API | +| `wallet-service` | — | Debit / Credit / ReverseDebit | +| `fx-service` | — | Settlement FX con timeout | +| `receipt-service` | — | Emisión de comprobantes | +| `query-service` | 3005 | CQRS read model | -### What disqualifies a solution +--- -- Calling `kafkaClient.emit()` directly inside a `@Transaction()` decorator. This is the most common mistake at this level and it produces silent data loss. +## Wallets de prueba -### Optional escalation +El servicio `db-seed` inserta automáticamente dos wallets al arrancar: -Add a per-country topic namespace (`pe.payments.payment.created.v1`, `mx.payments.payment.created.v1`) and document what that implies for consumer group strategy across countries. +| Wallet ID | Balance inicial | Moneda | +|-----------|----------------|--------| +| `a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11` | 100.00 PEN (10000 centavos) | PEN | +| `b0eebc99-9c0b-4ef8-bb6d-6bb9bd380a22` | 50.00 PEN (5000 centavos) | PEN | --- -## Challenge 2 — Wallet transfer with distributed saga +## Flujo completo — Happy path -### Premise +### 1. Iniciar una transferencia -Transferring funds between two wallets in different countries requires debiting one ledger and crediting another atomically — without a distributed transaction. You will implement a saga that is safe to replay from any step. +**Linux/macOS (curl):** +```bash +curl -s -X POST http://localhost:3001/transfers \ + -H "Content-Type: application/json" \ + -d '{ + "debitWalletId": "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11", + "creditWalletId": "b0eebc99-9c0b-4ef8-bb6d-6bb9bd380a22", + "amount": 10, + "currency": "PEN" + }' +``` -### Required deliverables +**Windows (PowerShell):** +```powershell +$body = @{ + debitWalletId = "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11" + creditWalletId = "b0eebc99-9c0b-4ef8-bb6d-6bb9bd380a22" + amount = 10.50 + currency = "USD" +} | ConvertTo-Json -1. **Transfer orchestrator** — Implement a `TransferOrchestrator` that drives the following steps in order: +Invoke-RestMethod -Uri http://127.0.0.1:3001/transfers -Method POST -ContentType "application/json" -Body $body +``` - ``` - DebitWallet → CreditWallet → SettleFX → EmitReceipt - ``` +Respuesta inmediata: +```json +{ "sagaId": "3b0e844c-e1a7-41f3-8ee5-9588a1bb6bd6" } +``` - You may use Temporal, a hand-rolled state machine, or pure Kafka choreography. You must justify the choice in writing. +### 2. Consultar el estado -2. **Compensation on failure** — If `CreditWallet` fails after `DebitWallet` succeeds, the orchestrator must issue a `ReverseDebit` compensation event. Silent failure is not acceptable. +**Linux/macOS:** +```bash +curl -s http://localhost:3005/transfers/3b0e844c-e1a7-41f3-8ee5-9588a1bb6bd6 +``` -3. **CQRS read model** — A `TransferReadModel` updated via projected events, not by reading the write-side database. The read model must be consistent enough to serve a GET within 500ms of the saga completing. +**Windows (PowerShell):** +```powershell +Invoke-RestMethod -Uri http://127.0.0.1:3005/transfers/3b0e844c-e1a7-41f3-8ee5-9588a1bb6bd6 +``` -4. **Concurrency safety** — If two transfers attempt to debit the same wallet simultaneously, the second must detect the conflict and fail fast. A negative balance is never acceptable. +El saga completa en ~1 segundo. La respuesta final: + +```json +{ + "sagaId": "3b0e844c-e1a7-41f3-8ee5-9588a1bb6bd6", + "status": "COMPLETED", + "debitWalletId": "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11", + "creditWalletId": "b0eebc99-9c0b-4ef8-bb6d-6bb9bd380a22", + "amount": 10, + "currency": "PEN", + "fxRate": 3.72, + "receiptId": "0bff698a-b86d-48d3-b88f-7d9198a31a48", + "failureReason": null, + "updatedAt": "2026-04-12T02:04:31.478Z", + "_consistency": { + "model": "eventual", + "eventVersion": 5 + } +} +``` -### What a strong solution looks like +`eventVersion: 5` confirma que se proyectaron los 5 eventos de estado: `PENDING → DEBITED → CREDITED → FX_SETTLED → COMPLETED`. -- The candidate picks a clear position on choreography vs. orchestration and can articulate the trade-off: choreography reduces coupling but makes the overall saga state invisible; orchestration makes state explicit but introduces a coordinator as a single point of failure. -- The idempotency key is placed on the saga instance, not on individual commands, and the candidate can explain why. -- The read model answers the question: "how do I know the read model isn't serving stale data immediately after the saga closes?" — whether via versioned events, a subscription mechanism, or a documented staleness window. +--- + +## Flujo de fallo — Compensación -### What disqualifies a solution +Para disparar una compensación, intenta debitar más de lo disponible: -A single database transaction spanning two service databases. This is the antipattern the challenge is explicitly designed to surface. +```bash +curl -s -X POST http://localhost:3001/transfers \ + -H "Content-Type: application/json" \ + -d '{ + "debitWalletId": "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11", + "creditWalletId": "b0eebc99-9c0b-4ef8-bb6d-6bb9bd380a22", + "amount": 9999, + "currency": "PEN" + }' +``` -### Optional escalation +El wallet-service detecta fondos insuficientes, emite `wallet.debit.failed`. El orquestador transiciona a `FAILED` (sin compensación porque el débito no se ejecutó): + +```json +{ + "status": "FAILED", + "failureReason": "INSUFFICIENT_FUNDS", + "_consistency": { "model": "eventual", "eventVersion": 2 } +} +``` -Model the FX settlement step as an external API call with a timeout. Show how the saga handles a timeout that leaves the FX state ambiguous — neither confirmed nor rejected. +Para una compensación real (debit ok → credit falla → reverse-debit), necesitarías un wallet de crédito bloqueado — la compensación `COMPENSATING → FAILED` se puede testear modificando el wallet de crédito directamente en la DB. --- -## Challenge 3 — Shared platform library design +## Verificar el flujo en Kafka -### Premise +Confirmar que todos los topics tienen mensajes tras una transferencia exitosa: -Your platform team owns the internal libraries that all product squads import. You've been asked to design and ship `@yape/kafka-module` — a NestJS dynamic module that wraps Kafka producer and consumer setup, enforces topic naming conventions, wires DLT automatically, and exposes typed event contracts. You are the only author. Four squads will consume it within the quarter. +```bash +docker exec wallet-transfer-saga-kafka-1 \ + kafka-run-class kafka.tools.GetOffsetShell \ + --broker-list localhost:9092 2>/dev/null | grep -v "^__" | grep -v ":0$" | sort +``` -### Required deliverables +Resultado esperado para un saga completo: -1. **Dynamic module API** — A `KafkaModule.forFeature({ topics, consumerGroup })` dynamic module. The module must register producers and consumers via NestJS dependency injection, not global singletons. +``` +fx.settle.cmd:0:1 +fx.settled:0:1 +receipt.emit.cmd:0:1 +receipt.emitted:0:1 +transfer.updated:0:5 +wallet.credit.cmd:0:1 +wallet.credit.succeeded:0:1 +wallet.debit.cmd:0:1 +wallet.debit.succeeded:0:1 +``` -2. **`@KafkaEvent()` decorator** — A `@KafkaEvent(topicName)` decorator that binds a handler method to a Kafka consumer, analogous to how NestJS `@MessagePattern` works internally. +--- -3. **Automatic DLT wiring** — If a handler throws and exceeds `maxRetries`, the module routes the message to `{original-topic}.dlt` without any code change required in the consuming squad. +## Tests -4. **`EventContract` type** — A generic type that enforces schema shape at compile time. Squads must not be able to publish to a topic with a payload that doesn't match the declared contract. A type mismatch must be a TypeScript compile error, not a runtime exception. +### Ejecutar todos los tests -5. **ADR (Architecture Decision Record)** — Written in MADR format, covering: - - Why NestJS dynamic modules over a plain exported class. - - How you handle schema evolution without breaking consumers who still reference an older version. - - What you would add with two more weeks. +```bash +# Desde la raíz del monorepo +npm test +``` -### What a strong solution looks like +Resultado esperado: +``` +Test Suites: 4 passed, 4 total +Tests: 35 passed, 35 total +``` -- The module API feels native to NestJS. A squad importing it should not need to understand Kafka internals to publish an event. -- Schema evolution is addressed concretely: additive fields, topic versioning (`payment.created.v2`), or a schema registry — the candidate picks one and defends it, with trade-offs acknowledged. -- The ADR reads like it was written for a real team, not as a post-hoc justification. It documents the options that were rejected and why. +### Suites incluidas + +| Suite | Archivo | Tests | +|-------|---------|-------| +| State machine | `apps/orchestrator/test/transfer-saga.orchestrator.spec.ts` | 7 | +| Wallet service | `apps/wallet-service/test/wallet.service.spec.ts` | 16 | +| FX service | `apps/fx-service/test/fx.service.spec.ts` | 6 | +| Money helper | `libs/helpers/money.helper.spec.ts` | 6 | + +### Qué cubren los tests + +**Orchestrator (`transfer-saga.orchestrator.spec.ts`):** +- `start()` persiste la saga y emite `DebitWallet` +- `onDebitSucceeded()` transiciona a `DEBITED` y emite `CreditWallet` +- `onDebitSucceeded()` es idempotente — si la transición falla (evento duplicado), no emite el siguiente comando +- `onDebitFailed()` transiciona a `FAILED` sin emitir compensación +- `onCreditFailed()` transiciona a `COMPENSATING` y emite `ReverseDebit` +- `onFxAmbiguous()` transiciona a `AMBIGUOUS` sin emitir `ReverseDebit` +- `onReceiptEmitted()` transiciona a `COMPLETED` +- Eventos out-of-order (saga no encontrada) se ignoran sin lanzar error + +**Wallet service (`wallet.service.spec.ts`):** +- Debit exitoso actualiza balance +- `INSUFFICIENT_FUNDS` lanzado cuando `amount > balance` +- Credit exitoso incrementa balance +- Reverse-debit restaura balance +- Idempotencia: operación duplicada (misma `idempotency_key`) no muta el balance +- Concurrencia: mock de transacciones concurrentes + +**FX service (`fx.service.spec.ts`):** +- Rate retornado cuando API responde dentro del timeout +- `fx.failed` emitido cuando API responde con error HTTP +- `fx.ambiguous` emitido cuando API supera el timeout + +**Money helper (`money.helper.spec.ts`):** +- `toCents` / `fromCents` sin pérdida de precisión +- `subtractMoney` lanza `INSUFFICIENT_FUNDS` correctamente +- `addMoney` acumula sin overflow -### What a weak solution looks like +--- -- The module wraps Kafka imperatively and tells squads to call `producer.send()` directly. -- `EventContract` is a runtime validation only (e.g. a Zod schema), with no compile-time enforcement. -- The ADR is a bulleted list with no trade-off reasoning. +## Resetear el entorno -### Optional escalation +```bash +# Detener todos los contenedores y eliminar volúmenes (DB + estado Kafka) +docker compose down -v -Publish the module to a local [Verdaccio](https://verdaccio.org/) registry. Document your versioning and release strategy, including how you would communicate breaking changes to consuming squads. +# Volver a levantar desde cero +docker compose up -d +``` --- -## Tech stack - -The following is the expected stack. Deviations are acceptable if you document the reason. +## Estructura del proyecto -| Layer | Expected | -|---|---| -| Runtime | Node.js 20+ | -| Framework | NestJS | -| Messaging | Kafka (local via Docker, or Confluent Cloud) | -| Database | Your choice — document why | -| Orchestration | Temporal, native Kafka, or a state machine — justify the choice | -| Language | TypeScript (strict mode) | -| Containers | Docker Compose for local environment | +``` +wallet-transfer-saga/ +├── apps/ +│ ├── orchestrator/ # State machine + POST /transfers (port 3001) +│ ├── wallet-service/ # Debit / Credit / ReverseDebit con SELECT FOR UPDATE +│ ├── fx-service/ # Settlement FX con timeout y estado ambiguo +│ ├── receipt-service/ # Emisión y persistencia de comprobantes +│ └── query-service/ # CQRS read model + GET /transfers/:id (port 3005) +├── libs/ +│ ├── contracts/ # Contratos Kafka: topics, commands, events (source of truth) +│ ├── common/ # Interfaces ISagaStep / ICompensable, filter, interceptor +│ └── helpers/ # money.helper (BigInt), saga-id, idempotency-key +├── scripts/ +│ └── seed.sql # Wallets de prueba (referencia — insertadas por db-seed en compose) +├── docker-compose.yml +├── Dockerfile.base # Imagen base monorepo con node_modules +├── .env.example # Variables de entorno (leídas directamente por compose) +└── ADR.md # Architecture Decision Record — todas las decisiones con código +``` --- -## Submission +## Limitaciones conocidas -1. Fork this repository. -2. Create a branch named `challenge/{your-name}`. -3. Open a pull request against `main` in this repository. +- **Sin outbox transaccional**: el estado del saga puede guardarse en DB sin que el mensaje Kafka correspondiente se envíe (ventana de fallo entre las dos operaciones). Documentado en ADR-008. +- **Una partición Kafka por tópico**: sin escalado horizontal de consumers por diseño del challenge. +- **Instancia PostgreSQL compartida**: en producción cada servicio tendría su propia DB. La separación lógica existe (conexiones TypeORM separadas, entidades distintas). +- **Estado `AMBIGUOUS` sin resolución automática**: requiere un job de reconciliación externo. +- **Sin autenticación en los endpoints HTTP**. -Your PR description must include: - -- Which challenge you chose and why. -- The key architectural decisions you made and the alternatives you rejected. -- What you would do differently with more time. -- Any known limitations or shortcuts taken. +--- -**There is no time limit stated intentionally.** A focused solution delivered in four hours tells us more than an exhaustive one delivered in two days. Prioritise depth of reasoning over breadth of features. +## Con más tiempo -If you have questions, open an issue on this repository. We respond to issues within one business day. +1. **Outbox pattern** para eliminar la ventana entre persistencia y emit de Kafka +2. **Dead letter topics** con alertas y UI de reprocesamiento +3. **Integration tests** con `testcontainers` (Postgres + Kafka reales en Docker) +4. **Job de reconciliación** para sagas en estado `AMBIGUOUS` +5. **DB por servicio** — cada microservicio con su propia instancia PostgreSQL diff --git a/apps/fx-service/Dockerfile b/apps/fx-service/Dockerfile new file mode 100644 index 00000000..7dde66b9 --- /dev/null +++ b/apps/fx-service/Dockerfile @@ -0,0 +1,9 @@ +FROM wallet-transfer-saga-base AS builder +WORKDIR /app/apps/fx-service +RUN ../../node_modules/.bin/nest build + +FROM node:22-alpine +WORKDIR /app +COPY --from=builder /app/apps/fx-service/dist/main.js ./main.js +COPY --from=builder /app/node_modules ./node_modules +CMD ["node", "main.js"] diff --git a/apps/fx-service/nest-cli.json b/apps/fx-service/nest-cli.json new file mode 100644 index 00000000..f683d65d --- /dev/null +++ b/apps/fx-service/nest-cli.json @@ -0,0 +1,9 @@ +{ + "collection": "@nestjs/schematics", + "sourceRoot": "src", + "compilerOptions": { + "webpack": true, + "webpackConfigPath": "webpack.config.js", + "tsConfigPath": "tsconfig.json" + } +} diff --git a/apps/fx-service/package.json b/apps/fx-service/package.json new file mode 100644 index 00000000..ce67a337 --- /dev/null +++ b/apps/fx-service/package.json @@ -0,0 +1,26 @@ +{ + "name": "@saga/fx-service", + "version": "1.0.0", + "scripts": { + "build": "nest build", + "start": "node dist/main", + "start:dev": "nest start --watch", + "test": "jest" + }, + "dependencies": { + "@nestjs/common": "^11.1.18", + "@nestjs/core": "^11.1.18", + "@nestjs/microservices": "^11.1.18", + "kafkajs": "^2.2.4", + "reflect-metadata": "^0.2.2", + "rxjs": "^7.8.1", + "uuid": "^11.0.0" + }, + "devDependencies": { + "@nestjs/cli": "^11.0.0", + "@nestjs/testing": "^11.1.18", + "@types/uuid": "^10.0.0", + "jest": "^29.7.0", + "ts-jest": "^29.2.0" + } +} diff --git a/apps/fx-service/src/app.module.ts b/apps/fx-service/src/app.module.ts new file mode 100644 index 00000000..5f49462c --- /dev/null +++ b/apps/fx-service/src/app.module.ts @@ -0,0 +1,28 @@ +import { Module } from '@nestjs/common'; +import { ClientsModule, Transport } from '@nestjs/microservices'; +import { FxModule } from './fx/fx.module'; +import { SettleFxConsumer } from './consumers/settle-fx.consumer'; + +@Module({ + imports: [ + ClientsModule.register([ + { + name: 'KAFKA_CLIENT', + transport: Transport.KAFKA, + options: { + client: { + clientId: process.env.KAFKA_CLIENT_ID, + brokers: (process.env.KAFKA_BROKERS ?? '').split(','), + }, + consumer: { + groupId: process.env.KAFKA_CONSUMER_GROUP_FX ?? 'fx-service-group', + }, + producerOnlyMode: true, + }, + }, + ]), + FxModule, + ], + controllers: [SettleFxConsumer], +}) +export class AppModule {} diff --git a/apps/fx-service/src/consumers/settle-fx.consumer.ts b/apps/fx-service/src/consumers/settle-fx.consumer.ts new file mode 100644 index 00000000..69c531e6 --- /dev/null +++ b/apps/fx-service/src/consumers/settle-fx.consumer.ts @@ -0,0 +1,60 @@ +import { Controller, Inject, Logger, OnModuleInit } from '@nestjs/common'; +import { EventPattern, Payload, ClientKafka } from '@nestjs/microservices'; +import { firstValueFrom } from 'rxjs'; +import { KafkaTopic } from '@contracts/topics'; +import { SettleFxCommand } from '@contracts/commands'; +import { FxSettledEvent, FxFailedEvent, FxAmbiguousEvent } from '@contracts/events'; +import { FxService } from '../fx/fx.service'; + +@Controller() +export class SettleFxConsumer implements OnModuleInit { + private readonly logger = new Logger(SettleFxConsumer.name); + + constructor( + private readonly fxService: FxService, + @Inject('KAFKA_CLIENT') private readonly kafka: ClientKafka, + ) {} + + async onModuleInit(): Promise { + await this.kafka.connect(); + } + + @EventPattern(KafkaTopic.SETTLE_FX) + async handle(@Payload() cmd: SettleFxCommand): Promise { + const result = await this.fxService.settle( + cmd.sagaId, + cmd.fromCurrency, + cmd.toCurrency, + ); + + if (result.status === 'settled') { + const event: FxSettledEvent = { + sagaId: cmd.sagaId, + fromCurrency: cmd.fromCurrency, + toCurrency: cmd.toCurrency, + rate: result.rate, + settledAt: result.settledAt, + }; + await firstValueFrom(this.kafka.emit(KafkaTopic.FX_SETTLED, event)); + return; + } + + if (result.status === 'ambiguous') { + const event: FxAmbiguousEvent = { + sagaId: cmd.sagaId, + timeoutMs: result.timeoutMs, + attemptedAt: result.attemptedAt, + }; + await firstValueFrom(this.kafka.emit(KafkaTopic.FX_AMBIGUOUS, event)); + this.logger.error(`FX ambiguous for saga ${cmd.sagaId} — timeout after ${result.timeoutMs}ms`); + return; + } + + const event: FxFailedEvent = { + sagaId: cmd.sagaId, + reason: result.reason, + }; + await firstValueFrom(this.kafka.emit(KafkaTopic.FX_FAILED, event)); + this.logger.warn(`FX failed for saga ${cmd.sagaId}: ${result.reason}`); + } +} diff --git a/apps/fx-service/src/fx/fx-rate.provider.ts b/apps/fx-service/src/fx/fx-rate.provider.ts new file mode 100644 index 00000000..4030b984 --- /dev/null +++ b/apps/fx-service/src/fx/fx-rate.provider.ts @@ -0,0 +1,44 @@ +import { Injectable, Logger } from '@nestjs/common'; + +export interface FxRateResponse { + rate: number; + settled: boolean; +} + +@Injectable() +export class FxRateProvider { + private readonly logger = new Logger(FxRateProvider.name); + private readonly apiUrl = process.env.FX_API_URL; + private readonly timeoutMs = parseInt(process.env.FX_TIMEOUT_MS ?? '5000'); + + async getRate(fromCurrency: string, toCurrency: string): Promise { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), this.timeoutMs); + + try { + const url = `${this.apiUrl}?from=${fromCurrency}&to=${toCurrency}`; + const response = await fetch(url, { signal: controller.signal }); + + if (!response.ok) { + throw new Error(`FX API responded with status ${response.status}`); + } + + return response.json() as Promise; + } catch (error) { + if (error instanceof Error && error.name === 'AbortError') { + this.logger.error(`FX API timed out after ${this.timeoutMs}ms`); + throw new FxTimeoutError(this.timeoutMs); + } + throw error; + } finally { + clearTimeout(timeoutId); + } + } +} + +export class FxTimeoutError extends Error { + constructor(public readonly timeoutMs: number) { + super(`FX API timed out after ${timeoutMs}ms`); + this.name = 'FxTimeoutError'; + } +} diff --git a/apps/fx-service/src/fx/fx.module.ts b/apps/fx-service/src/fx/fx.module.ts new file mode 100644 index 00000000..8797a501 --- /dev/null +++ b/apps/fx-service/src/fx/fx.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; +import { FxRateProvider } from './fx-rate.provider'; +import { FxService } from './fx.service'; + +@Module({ + providers: [FxRateProvider, FxService], + exports: [FxService], +}) +export class FxModule {} diff --git a/apps/fx-service/src/fx/fx.service.ts b/apps/fx-service/src/fx/fx.service.ts new file mode 100644 index 00000000..cd9d5123 --- /dev/null +++ b/apps/fx-service/src/fx/fx.service.ts @@ -0,0 +1,51 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { FxRateProvider, FxTimeoutError } from './fx-rate.provider'; +import { buildIdempotencyKey } from '@helpers/idempotency-key.helper'; + +export type FxSettleResult = + | { status: 'settled'; rate: number; settledAt: string } + | { status: 'failed'; reason: string } + | { status: 'ambiguous'; timeoutMs: number; attemptedAt: string }; + +@Injectable() +export class FxService { + private readonly logger = new Logger(FxService.name); + + // In-memory deduplication — evita llamar dos veces a la API externa por el mismo sagaId. + // En producción reemplazar con Redis o tabla DB para sobrevivir reinicios del servicio. + private readonly processed = new Map(); + + constructor(private readonly fxRateProvider: FxRateProvider) {} + + async settle(sagaId: string, fromCurrency: string, toCurrency: string): Promise { + const idempotencyKey = buildIdempotencyKey(sagaId, 'fx-settle'); + + const cached = this.processed.get(idempotencyKey); + if (cached) { + this.logger.log(`FX settle ${idempotencyKey} already processed — returning cached result`); + return cached; + } + + const result = await this.callProvider(fromCurrency, toCurrency); + this.processed.set(idempotencyKey, result); + return result; + } + + private async callProvider(fromCurrency: string, toCurrency: string): Promise { + try { + const { rate } = await this.fxRateProvider.getRate(fromCurrency, toCurrency); + return { status: 'settled', rate, settledAt: new Date().toISOString() }; + } catch (error) { + if (error instanceof FxTimeoutError) { + return { + status: 'ambiguous', + timeoutMs: error.timeoutMs, + attemptedAt: new Date().toISOString(), + }; + } + const reason = error instanceof Error ? error.message : 'UNKNOWN'; + this.logger.error(`FX provider error: ${reason}`); + return { status: 'failed', reason }; + } + } +} diff --git a/apps/fx-service/src/main.ts b/apps/fx-service/src/main.ts new file mode 100644 index 00000000..1c1f9780 --- /dev/null +++ b/apps/fx-service/src/main.ts @@ -0,0 +1,29 @@ +import { NestFactory } from '@nestjs/core'; +import { MicroserviceOptions, Transport } from '@nestjs/microservices'; +import { AppModule } from './app.module'; +import { KafkaExceptionFilter } from '@common/filters/kafka-exception.filter'; + +async function bootstrap(): Promise { + const app = await NestFactory.createMicroservice( + AppModule, + { + transport: Transport.KAFKA, + options: { + client: { + clientId: process.env.KAFKA_CLIENT_ID, + brokers: (process.env.KAFKA_BROKERS ?? '').split(','), + retry: { retries: 10, initialRetryTime: 300 }, + }, + consumer: { + groupId: process.env.KAFKA_CONSUMER_GROUP_FX ?? 'fx-service-group', + }, + }, + }, + ); + + app.enableShutdownHooks(); // drain in-flight messages before SIGTERM kills the process + app.useGlobalFilters(new KafkaExceptionFilter()); + await app.listen(); +} + +bootstrap(); diff --git a/apps/fx-service/test/fx.service.spec.ts b/apps/fx-service/test/fx.service.spec.ts new file mode 100644 index 00000000..5048be6d --- /dev/null +++ b/apps/fx-service/test/fx.service.spec.ts @@ -0,0 +1,76 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { FxService } from '../src/fx/fx.service'; +import { FxRateProvider, FxTimeoutError } from '../src/fx/fx-rate.provider'; + +const mockFxRateProvider = () => ({ + getRate: jest.fn(), +}); + +describe('FxService', () => { + let service: FxService; + let provider: ReturnType; + + beforeEach(async () => { + provider = mockFxRateProvider(); + + const module: TestingModule = await Test.createTestingModule({ + providers: [ + FxService, + { provide: FxRateProvider, useValue: provider }, + ], + }).compile(); + + service = module.get(FxService); + }); + + it('retorna settled con la tasa cuando el proveedor responde', async () => { + provider.getRate.mockResolvedValue({ rate: 3.72, settled: true }); + + const result = await service.settle('saga-001', 'PEN', 'USD'); + + expect(result.status).toBe('settled'); + if (result.status === 'settled') { + expect(result.rate).toBe(3.72); + } + }); + + it('retorna ambiguous cuando el proveedor hace timeout', async () => { + provider.getRate.mockRejectedValue(new FxTimeoutError(5000)); + + const result = await service.settle('saga-001', 'PEN', 'USD'); + + expect(result.status).toBe('ambiguous'); + if (result.status === 'ambiguous') { + expect(result.timeoutMs).toBe(5000); + } + }); + + it('retorna failed cuando el proveedor lanza un error genérico', async () => { + provider.getRate.mockRejectedValue(new Error('FX API responded with status 503')); + + const result = await service.settle('saga-001', 'PEN', 'USD'); + + expect(result.status).toBe('failed'); + if (result.status === 'failed') { + expect(result.reason).toContain('503'); + } + }); + + it('es idempotente — no llama al proveedor dos veces para el mismo sagaId', async () => { + provider.getRate.mockResolvedValue({ rate: 3.72, settled: true }); + + await service.settle('saga-002', 'PEN', 'USD'); + await service.settle('saga-002', 'PEN', 'USD'); + + expect(provider.getRate).toHaveBeenCalledTimes(1); + }); + + it('llama al proveedor para sagaIds distintos', async () => { + provider.getRate.mockResolvedValue({ rate: 3.72, settled: true }); + + await service.settle('saga-003', 'PEN', 'USD'); + await service.settle('saga-004', 'PEN', 'USD'); + + expect(provider.getRate).toHaveBeenCalledTimes(2); + }); +}); diff --git a/apps/fx-service/tsconfig.json b/apps/fx-service/tsconfig.json new file mode 100644 index 00000000..f3ed953e --- /dev/null +++ b/apps/fx-service/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "./dist", + "baseUrl": "../../" + }, + "include": ["src/**/*", "../../libs/**/*"] +} diff --git a/apps/fx-service/webpack.config.js b/apps/fx-service/webpack.config.js new file mode 100644 index 00000000..5452e190 --- /dev/null +++ b/apps/fx-service/webpack.config.js @@ -0,0 +1,20 @@ +const nodeExternals = require('webpack-node-externals'); +const { TsconfigPathsPlugin } = require('tsconfig-paths-webpack-plugin'); +const path = require('path'); + +module.exports = { + entry: './src/main.ts', + target: 'node', + externals: [nodeExternals({ modulesDir: path.resolve(__dirname, '../../node_modules') })], + resolve: { + extensions: ['.ts', '.js'], + plugins: [new TsconfigPathsPlugin({ configFile: './tsconfig.json' })], + }, + module: { + rules: [{ test: /\.ts$/, use: 'ts-loader', exclude: /node_modules/ }], + }, + output: { + filename: 'main.js', + path: path.resolve(__dirname, 'dist'), + }, +}; diff --git a/apps/orchestrator/Dockerfile b/apps/orchestrator/Dockerfile new file mode 100644 index 00000000..49ccf605 --- /dev/null +++ b/apps/orchestrator/Dockerfile @@ -0,0 +1,10 @@ +FROM wallet-transfer-saga-base AS builder +WORKDIR /app/apps/orchestrator +RUN ../../node_modules/.bin/nest build + +FROM node:22-alpine +WORKDIR /app +COPY --from=builder /app/apps/orchestrator/dist/main.js ./main.js +COPY --from=builder /app/node_modules ./node_modules +EXPOSE 3001 +CMD ["node", "main.js"] diff --git a/apps/orchestrator/nest-cli.json b/apps/orchestrator/nest-cli.json new file mode 100644 index 00000000..f683d65d --- /dev/null +++ b/apps/orchestrator/nest-cli.json @@ -0,0 +1,9 @@ +{ + "collection": "@nestjs/schematics", + "sourceRoot": "src", + "compilerOptions": { + "webpack": true, + "webpackConfigPath": "webpack.config.js", + "tsConfigPath": "tsconfig.json" + } +} diff --git a/apps/orchestrator/package.json b/apps/orchestrator/package.json new file mode 100644 index 00000000..653e62d9 --- /dev/null +++ b/apps/orchestrator/package.json @@ -0,0 +1,30 @@ +{ + "name": "@saga/orchestrator", + "version": "1.0.0", + "scripts": { + "build": "nest build", + "start": "node dist/main", + "start:dev": "nest start --watch", + "test": "jest" + }, + "dependencies": { + "@nestjs/common": "^11.1.18", + "@nestjs/core": "^11.1.18", + "@nestjs/microservices": "^11.1.18", + "@nestjs/platform-express": "^11.1.18", + "@nestjs/typeorm": "^11.0.0", + "kafkajs": "^2.2.4", + "typeorm": "^0.3.28", + "pg": "^8.13.0", + "reflect-metadata": "^0.2.2", + "rxjs": "^7.8.1", + "uuid": "^11.0.0" + }, + "devDependencies": { + "@nestjs/cli": "^11.0.0", + "@nestjs/testing": "^11.1.18", + "@types/uuid": "^10.0.0", + "jest": "^29.7.0", + "ts-jest": "^29.2.0" + } +} diff --git a/apps/orchestrator/src/app.module.ts b/apps/orchestrator/src/app.module.ts new file mode 100644 index 00000000..5b25937c --- /dev/null +++ b/apps/orchestrator/src/app.module.ts @@ -0,0 +1,41 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { TransferSagaEntity } from './saga/transfer-saga.entity'; +import { TransferSagaModule } from './saga/transfer-saga.module'; +import { TransferModule } from './transfer/transfer.module'; +import { DebitSucceededHandler } from './handlers/debit-succeeded.handler'; +import { DebitFailedHandler } from './handlers/debit-failed.handler'; +import { CreditSucceededHandler } from './handlers/credit-succeeded.handler'; +import { CreditFailedHandler } from './handlers/credit-failed.handler'; +import { FxSettledHandler } from './handlers/fx-settled.handler'; +import { FxFailedHandler } from './handlers/fx-failed.handler'; +import { FxAmbiguousHandler } from './handlers/fx-ambiguous.handler'; +import { ReceiptEmittedHandler } from './handlers/receipt-emitted.handler'; + +@Module({ + imports: [ + TypeOrmModule.forRoot({ + type: 'postgres', + host: process.env.POSTGRES_HOST, + port: parseInt(process.env.POSTGRES_PORT ?? '5432'), + username: process.env.POSTGRES_USER, + password: process.env.POSTGRES_PASSWORD, + database: process.env.POSTGRES_DB, + entities: [TransferSagaEntity], + synchronize: process.env.NODE_ENV !== 'production', + }), + TransferSagaModule, + TransferModule, + ], + controllers: [ + DebitSucceededHandler, + DebitFailedHandler, + CreditSucceededHandler, + CreditFailedHandler, + FxSettledHandler, + FxFailedHandler, + FxAmbiguousHandler, + ReceiptEmittedHandler, + ], +}) +export class AppModule {} diff --git a/apps/orchestrator/src/handlers/credit-failed.handler.ts b/apps/orchestrator/src/handlers/credit-failed.handler.ts new file mode 100644 index 00000000..72053b2b --- /dev/null +++ b/apps/orchestrator/src/handlers/credit-failed.handler.ts @@ -0,0 +1,15 @@ +import { Controller } from '@nestjs/common'; +import { EventPattern, Payload } from '@nestjs/microservices'; +import { KafkaTopic } from '@contracts/topics'; +import { CreditFailedEvent } from '@contracts/events'; +import { TransferSagaOrchestrator } from '../saga/transfer-saga.orchestrator'; + +@Controller() +export class CreditFailedHandler { + constructor(private readonly orchestrator: TransferSagaOrchestrator) {} + + @EventPattern(KafkaTopic.CREDIT_FAILED) + async handle(@Payload() event: CreditFailedEvent): Promise { + await this.orchestrator.onCreditFailed(event); + } +} diff --git a/apps/orchestrator/src/handlers/credit-succeeded.handler.ts b/apps/orchestrator/src/handlers/credit-succeeded.handler.ts new file mode 100644 index 00000000..e781929a --- /dev/null +++ b/apps/orchestrator/src/handlers/credit-succeeded.handler.ts @@ -0,0 +1,15 @@ +import { Controller } from '@nestjs/common'; +import { EventPattern, Payload } from '@nestjs/microservices'; +import { KafkaTopic } from '@contracts/topics'; +import { CreditSucceededEvent } from '@contracts/events'; +import { TransferSagaOrchestrator } from '../saga/transfer-saga.orchestrator'; + +@Controller() +export class CreditSucceededHandler { + constructor(private readonly orchestrator: TransferSagaOrchestrator) {} + + @EventPattern(KafkaTopic.CREDIT_SUCCEEDED) + async handle(@Payload() event: CreditSucceededEvent): Promise { + await this.orchestrator.onCreditSucceeded(event); + } +} diff --git a/apps/orchestrator/src/handlers/debit-failed.handler.ts b/apps/orchestrator/src/handlers/debit-failed.handler.ts new file mode 100644 index 00000000..bffb74ec --- /dev/null +++ b/apps/orchestrator/src/handlers/debit-failed.handler.ts @@ -0,0 +1,15 @@ +import { Controller } from '@nestjs/common'; +import { EventPattern, Payload } from '@nestjs/microservices'; +import { KafkaTopic } from '@contracts/topics'; +import { DebitFailedEvent } from '@contracts/events'; +import { TransferSagaOrchestrator } from '../saga/transfer-saga.orchestrator'; + +@Controller() +export class DebitFailedHandler { + constructor(private readonly orchestrator: TransferSagaOrchestrator) {} + + @EventPattern(KafkaTopic.DEBIT_FAILED) + async handle(@Payload() event: DebitFailedEvent): Promise { + await this.orchestrator.onDebitFailed(event); + } +} diff --git a/apps/orchestrator/src/handlers/debit-succeeded.handler.ts b/apps/orchestrator/src/handlers/debit-succeeded.handler.ts new file mode 100644 index 00000000..bb15227e --- /dev/null +++ b/apps/orchestrator/src/handlers/debit-succeeded.handler.ts @@ -0,0 +1,15 @@ +import { Controller } from '@nestjs/common'; +import { EventPattern, Payload } from '@nestjs/microservices'; +import { KafkaTopic } from '@contracts/topics'; +import { DebitSucceededEvent } from '@contracts/events'; +import { TransferSagaOrchestrator } from '../saga/transfer-saga.orchestrator'; + +@Controller() +export class DebitSucceededHandler { + constructor(private readonly orchestrator: TransferSagaOrchestrator) {} + + @EventPattern(KafkaTopic.DEBIT_SUCCEEDED) + async handle(@Payload() event: DebitSucceededEvent): Promise { + await this.orchestrator.onDebitSucceeded(event); + } +} diff --git a/apps/orchestrator/src/handlers/fx-ambiguous.handler.ts b/apps/orchestrator/src/handlers/fx-ambiguous.handler.ts new file mode 100644 index 00000000..e5050b89 --- /dev/null +++ b/apps/orchestrator/src/handlers/fx-ambiguous.handler.ts @@ -0,0 +1,15 @@ +import { Controller } from '@nestjs/common'; +import { EventPattern, Payload } from '@nestjs/microservices'; +import { KafkaTopic } from '@contracts/topics'; +import { FxAmbiguousEvent } from '@contracts/events'; +import { TransferSagaOrchestrator } from '../saga/transfer-saga.orchestrator'; + +@Controller() +export class FxAmbiguousHandler { + constructor(private readonly orchestrator: TransferSagaOrchestrator) {} + + @EventPattern(KafkaTopic.FX_AMBIGUOUS) + async handle(@Payload() event: FxAmbiguousEvent): Promise { + await this.orchestrator.onFxAmbiguous(event); + } +} diff --git a/apps/orchestrator/src/handlers/fx-failed.handler.ts b/apps/orchestrator/src/handlers/fx-failed.handler.ts new file mode 100644 index 00000000..f7c64237 --- /dev/null +++ b/apps/orchestrator/src/handlers/fx-failed.handler.ts @@ -0,0 +1,15 @@ +import { Controller } from '@nestjs/common'; +import { EventPattern, Payload } from '@nestjs/microservices'; +import { KafkaTopic } from '@contracts/topics'; +import { FxFailedEvent } from '@contracts/events'; +import { TransferSagaOrchestrator } from '../saga/transfer-saga.orchestrator'; + +@Controller() +export class FxFailedHandler { + constructor(private readonly orchestrator: TransferSagaOrchestrator) {} + + @EventPattern(KafkaTopic.FX_FAILED) + async handle(@Payload() event: FxFailedEvent): Promise { + await this.orchestrator.onFxFailed(event); + } +} diff --git a/apps/orchestrator/src/handlers/fx-settled.handler.ts b/apps/orchestrator/src/handlers/fx-settled.handler.ts new file mode 100644 index 00000000..dc6be5fd --- /dev/null +++ b/apps/orchestrator/src/handlers/fx-settled.handler.ts @@ -0,0 +1,15 @@ +import { Controller } from '@nestjs/common'; +import { EventPattern, Payload } from '@nestjs/microservices'; +import { KafkaTopic } from '@contracts/topics'; +import { FxSettledEvent } from '@contracts/events'; +import { TransferSagaOrchestrator } from '../saga/transfer-saga.orchestrator'; + +@Controller() +export class FxSettledHandler { + constructor(private readonly orchestrator: TransferSagaOrchestrator) {} + + @EventPattern(KafkaTopic.FX_SETTLED) + async handle(@Payload() event: FxSettledEvent): Promise { + await this.orchestrator.onFxSettled(event); + } +} diff --git a/apps/orchestrator/src/handlers/receipt-emitted.handler.ts b/apps/orchestrator/src/handlers/receipt-emitted.handler.ts new file mode 100644 index 00000000..b1a750c1 --- /dev/null +++ b/apps/orchestrator/src/handlers/receipt-emitted.handler.ts @@ -0,0 +1,15 @@ +import { Controller } from '@nestjs/common'; +import { EventPattern, Payload } from '@nestjs/microservices'; +import { KafkaTopic } from '@contracts/topics'; +import { ReceiptEmittedEvent } from '@contracts/events'; +import { TransferSagaOrchestrator } from '../saga/transfer-saga.orchestrator'; + +@Controller() +export class ReceiptEmittedHandler { + constructor(private readonly orchestrator: TransferSagaOrchestrator) {} + + @EventPattern(KafkaTopic.RECEIPT_EMITTED) + async handle(@Payload() event: ReceiptEmittedEvent): Promise { + await this.orchestrator.onReceiptEmitted(event); + } +} diff --git a/apps/orchestrator/src/main.ts b/apps/orchestrator/src/main.ts new file mode 100644 index 00000000..4abb319e --- /dev/null +++ b/apps/orchestrator/src/main.ts @@ -0,0 +1,35 @@ +import { NestFactory } from '@nestjs/core'; +import { MicroserviceOptions, Transport } from '@nestjs/microservices'; +import { ValidationPipe } from '@nestjs/common'; +import { AppModule } from './app.module'; +import { KafkaExceptionFilter } from '@common/filters/kafka-exception.filter'; +import { LoggingInterceptor } from '@common/interceptors/logging.interceptor'; + +async function bootstrap(): Promise { + const app = await NestFactory.create(AppModule); + + app.connectMicroservice({ + transport: Transport.KAFKA, + options: { + client: { + clientId: process.env.KAFKA_CLIENT_ID, + brokers: (process.env.KAFKA_BROKERS ?? '').split(','), + // Allow time for Kafka auto-create to propagate before giving up + retry: { retries: 10, initialRetryTime: 300 }, + }, + consumer: { + groupId: process.env.KAFKA_CONSUMER_GROUP_ORCHESTRATOR ?? 'orchestrator-group', + }, + }, + }); + + app.enableShutdownHooks(); // drain in-flight messages before SIGTERM kills the process + app.useGlobalPipes(new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true })); + app.useGlobalFilters(new KafkaExceptionFilter()); + app.useGlobalInterceptors(new LoggingInterceptor()); + + await app.startAllMicroservices(); + await app.listen(process.env.ORCHESTRATOR_PORT ?? 3001); +} + +bootstrap(); diff --git a/apps/orchestrator/src/saga/transfer-saga.entity.ts b/apps/orchestrator/src/saga/transfer-saga.entity.ts new file mode 100644 index 00000000..515bd62a --- /dev/null +++ b/apps/orchestrator/src/saga/transfer-saga.entity.ts @@ -0,0 +1,48 @@ +import { + Column, + CreateDateColumn, + Entity, + PrimaryColumn, + UpdateDateColumn, + VersionColumn, +} from 'typeorm'; +import { SagaState } from './transfer-saga.states'; + +@Entity('transfer_sagas') +export class TransferSagaEntity { + @PrimaryColumn('uuid') + id!: string; + + @Column({ unique: true }) + idempotencyKey!: string; + + @Column({ type: 'enum', enum: SagaState, default: SagaState.PENDING }) + state!: SagaState; + + @Column('uuid') + debitWalletId!: string; + + @Column('uuid') + creditWalletId!: string; + + @Column('bigint') + amount!: string; + + @Column({ length: 3 }) + currency!: string; // ISO 4217 + + @Column({ type: 'text', nullable: true }) + failureReason!: string | null; + + @Column({ type: 'float', nullable: true }) + fxRate!: number | null; + + @VersionColumn() + version!: number; + + @CreateDateColumn() + createdAt!: Date; + + @UpdateDateColumn() + updatedAt!: Date; +} diff --git a/apps/orchestrator/src/saga/transfer-saga.module.ts b/apps/orchestrator/src/saga/transfer-saga.module.ts new file mode 100644 index 00000000..cc190df7 --- /dev/null +++ b/apps/orchestrator/src/saga/transfer-saga.module.ts @@ -0,0 +1,32 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { ClientsModule, Transport } from '@nestjs/microservices'; +import { TransferSagaEntity } from './transfer-saga.entity'; +import { TransferSagaRepository } from './transfer-saga.repository'; +import { TransferSagaOrchestrator } from './transfer-saga.orchestrator'; + +@Module({ + imports: [ + TypeOrmModule.forFeature([TransferSagaEntity]), + ClientsModule.register([ + { + name: 'KAFKA_CLIENT', + transport: Transport.KAFKA, + options: { + client: { + clientId: process.env.KAFKA_CLIENT_ID, + brokers: (process.env.KAFKA_BROKERS ?? '').split(','), + retry: { retries: 10, initialRetryTime: 300 }, + }, + consumer: { + groupId: process.env.KAFKA_CONSUMER_GROUP_ORCHESTRATOR ?? 'orchestrator-group', + }, + producerOnlyMode: true, + }, + }, + ]), + ], + providers: [TransferSagaRepository, TransferSagaOrchestrator], + exports: [TransferSagaOrchestrator, TransferSagaRepository], +}) +export class TransferSagaModule {} diff --git a/apps/orchestrator/src/saga/transfer-saga.orchestrator.ts b/apps/orchestrator/src/saga/transfer-saga.orchestrator.ts new file mode 100644 index 00000000..ed94b87e --- /dev/null +++ b/apps/orchestrator/src/saga/transfer-saga.orchestrator.ts @@ -0,0 +1,239 @@ +import { Injectable, Logger, OnModuleInit } from '@nestjs/common'; +import { ClientKafka } from '@nestjs/microservices'; +import { Inject } from '@nestjs/common'; +import { firstValueFrom } from 'rxjs'; +import { KafkaTopic } from '@contracts/topics'; +import { + DebitWalletCommand, + CreditWalletCommand, + SettleFxCommand, + EmitReceiptCommand, + ReverseDebitCommand, +} from '@contracts/commands'; +import { + DebitSucceededEvent, + DebitFailedEvent, + CreditSucceededEvent, + CreditFailedEvent, + FxSettledEvent, + FxFailedEvent, + FxAmbiguousEvent, + ReceiptEmittedEvent, + TransferUpdatedEvent, +} from '@contracts/events'; +import { TransferSagaRepository } from './transfer-saga.repository'; +import { TransferSagaEntity } from './transfer-saga.entity'; +import { SagaState } from './transfer-saga.states'; + +@Injectable() +export class TransferSagaOrchestrator implements OnModuleInit { + private readonly logger = new Logger(TransferSagaOrchestrator.name); + + constructor( + private readonly sagaRepo: TransferSagaRepository, + @Inject('KAFKA_CLIENT') private readonly kafka: ClientKafka, + ) {} + + async onModuleInit(): Promise { + await this.kafka.connect(); + } + + async start(saga: TransferSagaEntity): Promise { + await this.sagaRepo.save(saga); + this.logger.log(`Saga ${saga.id} started`); + await this.publishUpdate(saga); + await this.emitDebit(saga); + } + + // ─── Step handlers ──────────────────────────────────────────────────────── + + async onDebitSucceeded(event: DebitSucceededEvent): Promise { + const saga = await this.loadSaga(event.sagaId); + if (!saga) return; + + const transitioned = await this.sagaRepo.transitionState( + saga.id, SagaState.PENDING, SagaState.DEBITED, saga.version, + ); + if (!transitioned) return; + + await this.publishUpdate({ ...saga, state: SagaState.DEBITED }); + await this.emitCredit(saga); + } + + async onDebitFailed(event: DebitFailedEvent): Promise { + const saga = await this.loadSaga(event.sagaId); + if (!saga) return; + + const transitioned = await this.sagaRepo.transitionState( + saga.id, SagaState.PENDING, SagaState.FAILED, saga.version, + { failureReason: event.reason }, + ); + if (!transitioned) return; + + await this.publishUpdate({ ...saga, state: SagaState.FAILED, failureReason: event.reason }); + } + + async onCreditSucceeded(event: CreditSucceededEvent): Promise { + const saga = await this.loadSaga(event.sagaId); + if (!saga) return; + + const transitioned = await this.sagaRepo.transitionState( + saga.id, SagaState.DEBITED, SagaState.CREDITED, saga.version, + ); + if (!transitioned) return; + + await this.publishUpdate({ ...saga, state: SagaState.CREDITED }); + await this.emitFx(saga); + } + + async onCreditFailed(event: CreditFailedEvent): Promise { + const saga = await this.loadSaga(event.sagaId); + if (!saga) return; + + const transitioned = await this.sagaRepo.transitionState( + saga.id, SagaState.DEBITED, SagaState.COMPENSATING, saga.version, + { failureReason: event.reason }, + ); + if (!transitioned) return; + + await this.publishUpdate({ ...saga, state: SagaState.COMPENSATING, failureReason: event.reason }); + await this.emitReverseDebit(saga, event.reason); + } + + async onFxSettled(event: FxSettledEvent): Promise { + const saga = await this.loadSaga(event.sagaId); + if (!saga) return; + + const transitioned = await this.sagaRepo.transitionState( + saga.id, SagaState.CREDITED, SagaState.FX_SETTLED, saga.version, + { fxRate: event.rate }, + ); + if (!transitioned) return; + + await this.publishUpdate({ ...saga, state: SagaState.FX_SETTLED, fxRate: event.rate }); + await this.emitReceipt(saga, event.rate); + } + + async onFxFailed(event: FxFailedEvent): Promise { + const saga = await this.loadSaga(event.sagaId); + if (!saga) return; + + const transitioned = await this.sagaRepo.transitionState( + saga.id, SagaState.CREDITED, SagaState.COMPENSATING, saga.version, + { failureReason: event.reason }, + ); + if (!transitioned) return; + + await this.publishUpdate({ ...saga, state: SagaState.COMPENSATING, failureReason: event.reason }); + await this.emitReverseDebit(saga, event.reason); + } + + async onFxAmbiguous(event: FxAmbiguousEvent): Promise { + const saga = await this.loadSaga(event.sagaId); + if (!saga) return; + + // No se compensa — el estado real del FX es desconocido. + // Un job de reconciliación externo debe resolverlo antes de reintentar o revertir. + const failureReason = `FX timeout after ${event.timeoutMs}ms`; + await this.sagaRepo.transitionState( + saga.id, SagaState.CREDITED, SagaState.AMBIGUOUS, saga.version, + { failureReason }, + ); + await this.publishUpdate({ ...saga, state: SagaState.AMBIGUOUS, failureReason }); + this.logger.error(`Saga ${saga.id} AMBIGUOUS — manual reconciliation required`); + } + + async onReceiptEmitted(event: ReceiptEmittedEvent): Promise { + const saga = await this.loadSaga(event.sagaId); + if (!saga) return; + + await this.sagaRepo.transitionState( + saga.id, SagaState.FX_SETTLED, SagaState.COMPLETED, saga.version, + ); + await this.publishUpdate({ ...saga, state: SagaState.COMPLETED }, event.receiptId); + this.logger.log(`Saga ${saga.id} COMPLETED`); + } + + // ─── Kafka emissions ────────────────────────────────────────────────────── + + private async emitDebit(saga: TransferSagaEntity): Promise { + const cmd: DebitWalletCommand = { + sagaId: saga.id, + walletId: saga.debitWalletId, + amount: saga.amount, + currency: saga.currency, + }; + await firstValueFrom(this.kafka.emit(KafkaTopic.DEBIT_WALLET, cmd)); + } + + private async emitCredit(saga: TransferSagaEntity): Promise { + const cmd: CreditWalletCommand = { + sagaId: saga.id, + walletId: saga.creditWalletId, + amount: saga.amount, + currency: saga.currency, + }; + await firstValueFrom(this.kafka.emit(KafkaTopic.CREDIT_WALLET, cmd)); + } + + private async emitFx(saga: TransferSagaEntity): Promise { + const cmd: SettleFxCommand = { + sagaId: saga.id, + fromCurrency: saga.currency, + toCurrency: saga.currency, + amount: saga.amount, + }; + await firstValueFrom(this.kafka.emit(KafkaTopic.SETTLE_FX, cmd)); + } + + private async emitReceipt(saga: TransferSagaEntity, fxRate: number): Promise { + const cmd: EmitReceiptCommand = { + sagaId: saga.id, + debitWalletId: saga.debitWalletId, + creditWalletId: saga.creditWalletId, + amount: saga.amount, + currency: saga.currency, + fxRate, + }; + await firstValueFrom(this.kafka.emit(KafkaTopic.EMIT_RECEIPT, cmd)); + } + + private async emitReverseDebit(saga: TransferSagaEntity, reason: string): Promise { + const cmd: ReverseDebitCommand = { + sagaId: saga.id, + walletId: saga.debitWalletId, + amount: saga.amount, + currency: saga.currency, + reason, + }; + await firstValueFrom(this.kafka.emit(KafkaTopic.REVERSE_DEBIT, cmd)); + } + + private async publishUpdate( + saga: Pick, + receiptId: string | null = null, + ): Promise { + const event: TransferUpdatedEvent = { + sagaId: saga.id, + state: saga.state, + debitWalletId: saga.debitWalletId, + creditWalletId: saga.creditWalletId, + amount: saga.amount, + currency: saga.currency, + fxRate: saga.fxRate, + receiptId, + failureReason: saga.failureReason, + }; + await firstValueFrom(this.kafka.emit(KafkaTopic.TRANSFER_UPDATED, event)); + } + + // ─── Internal ───────────────────────────────────────────────────────────── + + private async loadSaga(sagaId: string): Promise { + const saga = await this.sagaRepo.findById(sagaId); + if (!saga) { + this.logger.warn(`Saga ${sagaId} not found — possible out-of-order event`); + } + return saga; + } +} diff --git a/apps/orchestrator/src/saga/transfer-saga.repository.ts b/apps/orchestrator/src/saga/transfer-saga.repository.ts new file mode 100644 index 00000000..00881e53 --- /dev/null +++ b/apps/orchestrator/src/saga/transfer-saga.repository.ts @@ -0,0 +1,51 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { TransferSagaEntity } from './transfer-saga.entity'; +import { SagaState } from './transfer-saga.states'; + +@Injectable() +export class TransferSagaRepository { + constructor( + @InjectRepository(TransferSagaEntity) + private readonly repo: Repository, + ) {} + + async save(saga: TransferSagaEntity): Promise { + return this.repo.save(saga); + } + + async findById(id: string): Promise { + return this.repo.findOneBy({ id }); + } + + async findByIdempotencyKey(key: string): Promise { + return this.repo.findOneBy({ idempotencyKey: key }); + } + + /** + * Transición atómica de estado. Incluye expectedState y version en el WHERE + * para que dos eventos concurrentes del mismo saga no transicionen dos veces. + * Retorna true si la fila fue actualizada, false si otro proceso se adelantó. + */ + async transitionState( + id: string, + expectedState: SagaState, + nextState: SagaState, + version: number, + extra: Partial = {}, + ): Promise { + const result = await this.repo + .createQueryBuilder() + .update(TransferSagaEntity) + .set({ state: nextState, ...extra }) + .where('id = :id AND state = :expectedState AND version = :version', { + id, + expectedState, + version, + }) + .execute(); + + return (result.affected ?? 0) > 0; + } +} diff --git a/apps/orchestrator/src/saga/transfer-saga.states.ts b/apps/orchestrator/src/saga/transfer-saga.states.ts new file mode 100644 index 00000000..3655e575 --- /dev/null +++ b/apps/orchestrator/src/saga/transfer-saga.states.ts @@ -0,0 +1,10 @@ +export enum SagaState { + PENDING = 'PENDING', + DEBITED = 'DEBITED', + CREDITED = 'CREDITED', + FX_SETTLED = 'FX_SETTLED', + COMPLETED = 'COMPLETED', + COMPENSATING = 'COMPENSATING', // ReverseDebit en curso + AMBIGUOUS = 'AMBIGUOUS', // FX no respondió — requiere reconciliación manual + FAILED = 'FAILED', +} diff --git a/apps/orchestrator/src/transfer/transfer.controller.ts b/apps/orchestrator/src/transfer/transfer.controller.ts new file mode 100644 index 00000000..08c3bc3f --- /dev/null +++ b/apps/orchestrator/src/transfer/transfer.controller.ts @@ -0,0 +1,14 @@ +import { Body, Controller, Post, HttpCode, HttpStatus } from '@nestjs/common'; +import { TransferService } from './transfer.service'; +import { CreateTransferDto } from './transfer.dto'; + +@Controller('transfers') +export class TransferController { + constructor(private readonly transferService: TransferService) {} + + @Post() + @HttpCode(HttpStatus.ACCEPTED) + async create(@Body() dto: CreateTransferDto): Promise<{ sagaId: string }> { + return this.transferService.initiate(dto); + } +} diff --git a/apps/orchestrator/src/transfer/transfer.dto.ts b/apps/orchestrator/src/transfer/transfer.dto.ts new file mode 100644 index 00000000..9e74f7f2 --- /dev/null +++ b/apps/orchestrator/src/transfer/transfer.dto.ts @@ -0,0 +1,18 @@ +import { IsUUID, IsNumber, IsPositive, IsString, Length, NotEquals } from 'class-validator'; + +export class CreateTransferDto { + @IsUUID() + debitWalletId!: string; + + @IsUUID() + @NotEquals(undefined) // validado más adelante — wallets distintos es regla de negocio + creditWalletId!: string; + + @IsNumber({ maxDecimalPlaces: 2 }) + @IsPositive() + amount!: number; // decimal del cliente (ej: 10.50) — se convierte a centavos en el service + + @IsString() + @Length(3, 3) + currency!: string; // ISO 4217 +} diff --git a/apps/orchestrator/src/transfer/transfer.module.ts b/apps/orchestrator/src/transfer/transfer.module.ts new file mode 100644 index 00000000..028be907 --- /dev/null +++ b/apps/orchestrator/src/transfer/transfer.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { TransferController } from './transfer.controller'; +import { TransferService } from './transfer.service'; +import { TransferSagaModule } from '../saga/transfer-saga.module'; + +@Module({ + imports: [TransferSagaModule], + controllers: [TransferController], + providers: [TransferService], +}) +export class TransferModule {} diff --git a/apps/orchestrator/src/transfer/transfer.service.ts b/apps/orchestrator/src/transfer/transfer.service.ts new file mode 100644 index 00000000..81f9a022 --- /dev/null +++ b/apps/orchestrator/src/transfer/transfer.service.ts @@ -0,0 +1,43 @@ +import { Injectable, BadRequestException } from '@nestjs/common'; +import { TransferSagaOrchestrator } from '../saga/transfer-saga.orchestrator'; +import { TransferSagaRepository } from '../saga/transfer-saga.repository'; +import { TransferSagaEntity } from '../saga/transfer-saga.entity'; +import { SagaState } from '../saga/transfer-saga.states'; +import { generateSagaId } from '@helpers/saga-id.helper'; +import { buildIdempotencyKey } from '@helpers/idempotency-key.helper'; +import { toCents } from '@helpers/money.helper'; +import { CreateTransferDto } from './transfer.dto'; + +@Injectable() +export class TransferService { + constructor( + private readonly orchestrator: TransferSagaOrchestrator, + private readonly sagaRepo: TransferSagaRepository, + ) {} + + async initiate(dto: CreateTransferDto): Promise<{ sagaId: string }> { + if (dto.debitWalletId === dto.creditWalletId) { + throw new BadRequestException('debitWalletId and creditWalletId must be different'); + } + + const sagaId = generateSagaId(); + const idempotencyKey = buildIdempotencyKey(sagaId, 'init'); + + const existing = await this.sagaRepo.findByIdempotencyKey(idempotencyKey); + if (existing) return { sagaId: existing.id }; + + const saga = new TransferSagaEntity(); + saga.id = sagaId; + saga.idempotencyKey = idempotencyKey; + saga.state = SagaState.PENDING; + saga.debitWalletId = dto.debitWalletId; + saga.creditWalletId = dto.creditWalletId; + saga.amount = toCents(dto.amount).toString(); + saga.currency = dto.currency.toUpperCase(); + saga.failureReason = null; + saga.fxRate = null; + + await this.orchestrator.start(saga); + return { sagaId }; + } +} diff --git a/apps/orchestrator/test/transfer-saga.orchestrator.spec.ts b/apps/orchestrator/test/transfer-saga.orchestrator.spec.ts new file mode 100644 index 00000000..903915d0 --- /dev/null +++ b/apps/orchestrator/test/transfer-saga.orchestrator.spec.ts @@ -0,0 +1,177 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { of } from 'rxjs'; +import { TransferSagaOrchestrator } from '../src/saga/transfer-saga.orchestrator'; +import { TransferSagaRepository } from '../src/saga/transfer-saga.repository'; +import { TransferSagaEntity } from '../src/saga/transfer-saga.entity'; +import { SagaState } from '../src/saga/transfer-saga.states'; + +const mockRepo = () => ({ + save: jest.fn(), + findById: jest.fn(), + transitionState: jest.fn(), +}); + +// emit() debe retornar un Observable para que firstValueFrom() pueda suscribirse +const mockKafka = () => ({ + emit: jest.fn().mockReturnValue(of(null)), + connect: jest.fn().mockResolvedValue(undefined), +}); + +const buildSaga = (state: SagaState, version = 1): TransferSagaEntity => { + const saga = new TransferSagaEntity(); + saga.id = 'saga-001'; + saga.idempotencyKey = 'saga-001::init'; + saga.state = state; + saga.debitWalletId = 'wallet-debit'; + saga.creditWalletId = 'wallet-credit'; + saga.amount = '10000'; + saga.currency = 'PEN'; + saga.fxRate = null; + saga.failureReason = null; + saga.version = version; + return saga; +}; + +describe('TransferSagaOrchestrator', () => { + let orchestrator: TransferSagaOrchestrator; + let repo: ReturnType; + let kafka: ReturnType; + + beforeEach(async () => { + repo = mockRepo(); + kafka = mockKafka(); + + const module: TestingModule = await Test.createTestingModule({ + providers: [ + TransferSagaOrchestrator, + { provide: TransferSagaRepository, useValue: repo }, + { provide: 'KAFKA_CLIENT', useValue: kafka }, + ], + }).compile(); + + orchestrator = module.get(TransferSagaOrchestrator); + }); + + describe('start', () => { + it('persiste la saga y emite DebitWallet', async () => { + const saga = buildSaga(SagaState.PENDING); + repo.save.mockResolvedValue(saga); + + await orchestrator.start(saga); + + expect(repo.save).toHaveBeenCalledWith(saga); + expect(kafka.emit).toHaveBeenCalledWith( + 'wallet.debit.cmd', + expect.objectContaining({ sagaId: saga.id, walletId: saga.debitWalletId }), + ); + }); + }); + + describe('onDebitSucceeded', () => { + it('transiciona a DEBITED y emite CreditWallet', async () => { + const saga = buildSaga(SagaState.PENDING); + repo.findById.mockResolvedValue(saga); + repo.transitionState.mockResolvedValue(true); + + await orchestrator.onDebitSucceeded({ sagaId: saga.id, walletId: saga.debitWalletId, amount: '10000', balanceAfter: '0' }); + + expect(repo.transitionState).toHaveBeenCalledWith( + saga.id, SagaState.PENDING, SagaState.DEBITED, saga.version, + ); + expect(kafka.emit).toHaveBeenCalledWith( + 'wallet.credit.cmd', + expect.objectContaining({ sagaId: saga.id }), + ); + }); + + it('no emite CreditWallet si la transición falla (evento duplicado)', async () => { + const saga = buildSaga(SagaState.PENDING); + repo.findById.mockResolvedValue(saga); + repo.transitionState.mockResolvedValue(false); // otro proceso ganó la carrera + + await orchestrator.onDebitSucceeded({ sagaId: saga.id, walletId: saga.debitWalletId, amount: '10000', balanceAfter: '0' }); + + expect(kafka.emit).not.toHaveBeenCalled(); + }); + }); + + describe('onDebitFailed', () => { + it('transiciona a FAILED sin emitir comandos de compensación', async () => { + const saga = buildSaga(SagaState.PENDING); + repo.findById.mockResolvedValue(saga); + repo.transitionState.mockResolvedValue(true); + + await orchestrator.onDebitFailed({ sagaId: saga.id, walletId: saga.debitWalletId, reason: 'INSUFFICIENT_FUNDS' }); + + expect(repo.transitionState).toHaveBeenCalledWith( + saga.id, SagaState.PENDING, SagaState.FAILED, saga.version, + { failureReason: 'INSUFFICIENT_FUNDS' }, + ); + // Solo emite transfer.updated — no reverse-debit ni ningún otro comando + expect(kafka.emit).not.toHaveBeenCalledWith('wallet.reverse-debit.cmd', expect.anything()); + expect(kafka.emit).toHaveBeenCalledWith('transfer.updated', expect.objectContaining({ sagaId: saga.id })); + }); + }); + + describe('onCreditFailed', () => { + it('transiciona a COMPENSATING y emite ReverseDebit', async () => { + const saga = buildSaga(SagaState.DEBITED); + repo.findById.mockResolvedValue(saga); + repo.transitionState.mockResolvedValue(true); + + await orchestrator.onCreditFailed({ sagaId: saga.id, walletId: saga.creditWalletId, reason: 'WALLET_LOCKED' }); + + expect(repo.transitionState).toHaveBeenCalledWith( + saga.id, SagaState.DEBITED, SagaState.COMPENSATING, saga.version, + { failureReason: 'WALLET_LOCKED' }, + ); + expect(kafka.emit).toHaveBeenCalledWith( + 'wallet.reverse-debit.cmd', + expect.objectContaining({ sagaId: saga.id, reason: 'WALLET_LOCKED' }), + ); + expect(kafka.emit).toHaveBeenCalledWith('transfer.updated', expect.objectContaining({ sagaId: saga.id })); + }); + }); + + describe('onFxAmbiguous', () => { + it('transiciona a AMBIGUOUS sin emitir compensación', async () => { + const saga = buildSaga(SagaState.CREDITED); + repo.findById.mockResolvedValue(saga); + repo.transitionState.mockResolvedValue(true); + + await orchestrator.onFxAmbiguous({ sagaId: saga.id, timeoutMs: 5000, attemptedAt: '2026-04-11T00:00:00Z' }); + + expect(repo.transitionState).toHaveBeenCalledWith( + saga.id, SagaState.CREDITED, SagaState.AMBIGUOUS, saga.version, + { failureReason: 'FX timeout after 5000ms' }, + ); + // No se emite reverse-debit — el estado ambiguo requiere reconciliación manual + expect(kafka.emit).not.toHaveBeenCalledWith('wallet.reverse-debit.cmd', expect.anything()); + expect(kafka.emit).toHaveBeenCalledWith('transfer.updated', expect.objectContaining({ sagaId: saga.id })); + }); + }); + + describe('onReceiptEmitted', () => { + it('transiciona a COMPLETED', async () => { + const saga = buildSaga(SagaState.FX_SETTLED); + repo.findById.mockResolvedValue(saga); + repo.transitionState.mockResolvedValue(true); + + await orchestrator.onReceiptEmitted({ sagaId: saga.id, receiptId: 'receipt-001', emittedAt: '2026-04-11T00:00:00Z' }); + + expect(repo.transitionState).toHaveBeenCalledWith( + saga.id, SagaState.FX_SETTLED, SagaState.COMPLETED, saga.version, + ); + }); + }); + + describe('saga no encontrada', () => { + it('no lanza error si la saga no existe — evento out-of-order ignorado', async () => { + repo.findById.mockResolvedValue(null); + + await expect( + orchestrator.onDebitSucceeded({ sagaId: 'inexistente', walletId: 'w1', amount: '100', balanceAfter: '0' }), + ).resolves.not.toThrow(); + }); + }); +}); diff --git a/apps/orchestrator/tsconfig.json b/apps/orchestrator/tsconfig.json new file mode 100644 index 00000000..f3ed953e --- /dev/null +++ b/apps/orchestrator/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "./dist", + "baseUrl": "../../" + }, + "include": ["src/**/*", "../../libs/**/*"] +} diff --git a/apps/orchestrator/webpack.config.js b/apps/orchestrator/webpack.config.js new file mode 100644 index 00000000..5452e190 --- /dev/null +++ b/apps/orchestrator/webpack.config.js @@ -0,0 +1,20 @@ +const nodeExternals = require('webpack-node-externals'); +const { TsconfigPathsPlugin } = require('tsconfig-paths-webpack-plugin'); +const path = require('path'); + +module.exports = { + entry: './src/main.ts', + target: 'node', + externals: [nodeExternals({ modulesDir: path.resolve(__dirname, '../../node_modules') })], + resolve: { + extensions: ['.ts', '.js'], + plugins: [new TsconfigPathsPlugin({ configFile: './tsconfig.json' })], + }, + module: { + rules: [{ test: /\.ts$/, use: 'ts-loader', exclude: /node_modules/ }], + }, + output: { + filename: 'main.js', + path: path.resolve(__dirname, 'dist'), + }, +}; diff --git a/apps/query-service/Dockerfile b/apps/query-service/Dockerfile new file mode 100644 index 00000000..f33519e6 --- /dev/null +++ b/apps/query-service/Dockerfile @@ -0,0 +1,10 @@ +FROM wallet-transfer-saga-base AS builder +WORKDIR /app/apps/query-service +RUN ../../node_modules/.bin/nest build + +FROM node:22-alpine +WORKDIR /app +COPY --from=builder /app/apps/query-service/dist/main.js ./main.js +COPY --from=builder /app/node_modules ./node_modules +EXPOSE 3005 +CMD ["node", "main.js"] diff --git a/apps/query-service/nest-cli.json b/apps/query-service/nest-cli.json new file mode 100644 index 00000000..f683d65d --- /dev/null +++ b/apps/query-service/nest-cli.json @@ -0,0 +1,9 @@ +{ + "collection": "@nestjs/schematics", + "sourceRoot": "src", + "compilerOptions": { + "webpack": true, + "webpackConfigPath": "webpack.config.js", + "tsConfigPath": "tsconfig.json" + } +} diff --git a/apps/query-service/package.json b/apps/query-service/package.json new file mode 100644 index 00000000..3c0a7d43 --- /dev/null +++ b/apps/query-service/package.json @@ -0,0 +1,30 @@ +{ + "name": "@saga/query-service", + "version": "1.0.0", + "scripts": { + "build": "nest build", + "start": "node dist/main", + "start:dev": "nest start --watch", + "test": "jest" + }, + "dependencies": { + "@nestjs/common": "^11.1.18", + "@nestjs/core": "^11.1.18", + "@nestjs/microservices": "^11.1.18", + "@nestjs/platform-express": "^11.1.18", + "@nestjs/typeorm": "^11.0.0", + "kafkajs": "^2.2.4", + "typeorm": "^0.3.28", + "pg": "^8.13.0", + "reflect-metadata": "^0.2.2", + "rxjs": "^7.8.1", + "uuid": "^11.0.0" + }, + "devDependencies": { + "@nestjs/cli": "^11.0.0", + "@nestjs/testing": "^11.1.18", + "@types/uuid": "^10.0.0", + "jest": "^29.7.0", + "ts-jest": "^29.2.0" + } +} diff --git a/apps/query-service/src/app.module.ts b/apps/query-service/src/app.module.ts new file mode 100644 index 00000000..bc2ca51a --- /dev/null +++ b/apps/query-service/src/app.module.ts @@ -0,0 +1,41 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { ClientsModule, Transport } from '@nestjs/microservices'; +import { TransferReadModelEntity } from './read-model/transfer-read-model.entity'; +import { TransferReadModelModule } from './read-model/transfer-read-model.module'; +import { ProjectionModule } from './projections/projection.module'; +import { TransferQueryModule } from './query/transfer-query.module'; + +@Module({ + imports: [ + TypeOrmModule.forRoot({ + type: 'postgres', + host: process.env.POSTGRES_HOST, + port: parseInt(process.env.POSTGRES_PORT ?? '5432'), + username: process.env.POSTGRES_USER, + password: process.env.POSTGRES_PASSWORD, + database: process.env.POSTGRES_DB, + entities: [TransferReadModelEntity], + synchronize: process.env.NODE_ENV !== 'production', + }), + ClientsModule.register([ + { + name: 'KAFKA_CLIENT', + transport: Transport.KAFKA, + options: { + client: { + clientId: process.env.KAFKA_CLIENT_ID, + brokers: (process.env.KAFKA_BROKERS ?? '').split(','), + }, + consumer: { + groupId: process.env.KAFKA_CONSUMER_GROUP_QUERY ?? 'query-service-group', + }, + }, + }, + ]), + TransferReadModelModule, + ProjectionModule, + TransferQueryModule, + ], +}) +export class AppModule {} diff --git a/apps/query-service/src/main.ts b/apps/query-service/src/main.ts new file mode 100644 index 00000000..3f8b2b35 --- /dev/null +++ b/apps/query-service/src/main.ts @@ -0,0 +1,32 @@ +import { NestFactory } from '@nestjs/core'; +import { MicroserviceOptions, Transport } from '@nestjs/microservices'; +import { AppModule } from './app.module'; +import { KafkaExceptionFilter } from '@common/filters/kafka-exception.filter'; +import { LoggingInterceptor } from '@common/interceptors/logging.interceptor'; + +async function bootstrap(): Promise { + const app = await NestFactory.create(AppModule); + + app.connectMicroservice({ + transport: Transport.KAFKA, + options: { + client: { + clientId: process.env.KAFKA_CLIENT_ID, + brokers: (process.env.KAFKA_BROKERS ?? '').split(','), + retry: { retries: 10, initialRetryTime: 300 }, + }, + consumer: { + groupId: process.env.KAFKA_CONSUMER_GROUP_QUERY ?? 'query-service-group', + }, + }, + }); + + app.enableShutdownHooks(); // drain in-flight messages before SIGTERM kills the process + app.useGlobalFilters(new KafkaExceptionFilter()); + app.useGlobalInterceptors(new LoggingInterceptor()); + + await app.startAllMicroservices(); + await app.listen(process.env.QUERY_SERVICE_PORT ?? 3005); +} + +bootstrap(); diff --git a/apps/query-service/src/projections/projection.module.ts b/apps/query-service/src/projections/projection.module.ts new file mode 100644 index 00000000..4a1c10a8 --- /dev/null +++ b/apps/query-service/src/projections/projection.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; +import { TransferProjectionHandler } from './transfer-projection.handler'; +import { TransferReadModelModule } from '../read-model/transfer-read-model.module'; + +@Module({ + imports: [TransferReadModelModule], + controllers: [TransferProjectionHandler], +}) +export class ProjectionModule {} diff --git a/apps/query-service/src/projections/transfer-projection.handler.ts b/apps/query-service/src/projections/transfer-projection.handler.ts new file mode 100644 index 00000000..ce7684be --- /dev/null +++ b/apps/query-service/src/projections/transfer-projection.handler.ts @@ -0,0 +1,49 @@ +import { Controller, Logger } from '@nestjs/common'; +import { EventPattern, Payload } from '@nestjs/microservices'; +import { KafkaTopic } from '@contracts/topics'; +import { TransferUpdatedEvent } from '@contracts/events'; +import { TransferReadModelRepository } from '../read-model/transfer-read-model.repository'; +import { TransferReadModelEntity, TransferReadStatus } from '../read-model/transfer-read-model.entity'; + +// Maps orchestrator SagaState strings to query-side read status. +const stateToStatus: Record = { + PENDING: TransferReadStatus.PENDING, + DEBITED: TransferReadStatus.DEBITED, + CREDITED: TransferReadStatus.CREDITED, + FX_SETTLED: TransferReadStatus.FX_SETTLED, + COMPLETED: TransferReadStatus.COMPLETED, + COMPENSATING: TransferReadStatus.COMPENSATING, + AMBIGUOUS: TransferReadStatus.AMBIGUOUS, + FAILED: TransferReadStatus.FAILED, +}; + +@Controller() +export class TransferProjectionHandler { + private readonly logger = new Logger(TransferProjectionHandler.name); + + constructor(private readonly readModelRepo: TransferReadModelRepository) {} + + @EventPattern(KafkaTopic.TRANSFER_UPDATED) + async onTransferUpdated(@Payload() event: TransferUpdatedEvent): Promise { + try { + const existing = await this.readModelRepo.findBySagaId(event.sagaId); + const model = existing ?? new TransferReadModelEntity(); + + model.sagaId = event.sagaId; + model.status = stateToStatus[event.state] ?? TransferReadStatus.PENDING; + model.debitWalletId = event.debitWalletId; + model.creditWalletId = event.creditWalletId; + model.amount = event.amount; + model.currency = event.currency; + model.fxRate = event.fxRate; + model.receiptId = event.receiptId; + model.failureReason = event.failureReason; + model.eventVersion = (model.eventVersion ?? 0) + 1; + + await this.readModelRepo.upsert(model); + } catch (error) { + this.logger.error(`Projection failed for saga ${event.sagaId}`, error); + throw error; + } + } +} diff --git a/apps/query-service/src/query/transfer-query.controller.ts b/apps/query-service/src/query/transfer-query.controller.ts new file mode 100644 index 00000000..a4628d11 --- /dev/null +++ b/apps/query-service/src/query/transfer-query.controller.ts @@ -0,0 +1,13 @@ +import { Controller, Get, Param } from '@nestjs/common'; +import { TransferQueryService } from './transfer-query.service'; +import { TransferQueryResult } from './transfer-query.service'; + +@Controller('transfers') +export class TransferQueryController { + constructor(private readonly queryService: TransferQueryService) {} + + @Get(':sagaId') + async findOne(@Param('sagaId') sagaId: string): Promise { + return this.queryService.findBySagaId(sagaId); + } +} diff --git a/apps/query-service/src/query/transfer-query.module.ts b/apps/query-service/src/query/transfer-query.module.ts new file mode 100644 index 00000000..1a1d2410 --- /dev/null +++ b/apps/query-service/src/query/transfer-query.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { TransferQueryController } from './transfer-query.controller'; +import { TransferQueryService } from './transfer-query.service'; +import { TransferReadModelModule } from '../read-model/transfer-read-model.module'; + +@Module({ + imports: [TransferReadModelModule], + controllers: [TransferQueryController], + providers: [TransferQueryService], +}) +export class TransferQueryModule {} diff --git a/apps/query-service/src/query/transfer-query.service.ts b/apps/query-service/src/query/transfer-query.service.ts new file mode 100644 index 00000000..87f3ab95 --- /dev/null +++ b/apps/query-service/src/query/transfer-query.service.ts @@ -0,0 +1,54 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; +import { TransferReadModelRepository } from '../read-model/transfer-read-model.repository'; +import { TransferReadModelEntity } from '../read-model/transfer-read-model.entity'; +import { fromCents } from '@helpers/money.helper'; + +export interface TransferQueryResult { + sagaId: string; + status: string; + debitWalletId: string; + creditWalletId: string; + amount: number; + currency: string; + fxRate: number | null; + receiptId: string | null; + failureReason: string | null; + updatedAt: Date; + _consistency: { + // El read model se actualiza vía proyección de eventos Kafka. + // La ventana de inconsistencia es el lag del consumer group query-service-group. + // En condiciones normales < 500ms. Monitorear con kafka.consumer.lag en métricas. + model: 'eventual'; + eventVersion: number; + }; +} + +@Injectable() +export class TransferQueryService { + constructor(private readonly readModelRepo: TransferReadModelRepository) {} + + async findBySagaId(sagaId: string): Promise { + const model = await this.readModelRepo.findBySagaId(sagaId); + if (!model) throw new NotFoundException(`Transfer ${sagaId} not found`); + return this.toResult(model); + } + + private toResult(model: TransferReadModelEntity): TransferQueryResult { + return { + sagaId: model.sagaId, + status: model.status, + debitWalletId: model.debitWalletId, + creditWalletId: model.creditWalletId, + amount: fromCents(BigInt(model.amount)), + currency: model.currency, + fxRate: model.fxRate, + receiptId: model.receiptId, + failureReason: model.failureReason, + updatedAt: model.updatedAt, + _consistency: { + model: 'eventual', + eventVersion: model.eventVersion, + }, + }; + } +} diff --git a/apps/query-service/src/read-model/transfer-read-model.entity.ts b/apps/query-service/src/read-model/transfer-read-model.entity.ts new file mode 100644 index 00000000..280a8922 --- /dev/null +++ b/apps/query-service/src/read-model/transfer-read-model.entity.ts @@ -0,0 +1,55 @@ +import { + Column, + Entity, + PrimaryColumn, + UpdateDateColumn, +} from 'typeorm'; + +export enum TransferReadStatus { + PENDING = 'PENDING', + DEBITED = 'DEBITED', + CREDITED = 'CREDITED', + FX_SETTLED = 'FX_SETTLED', + COMPLETED = 'COMPLETED', + COMPENSATING = 'COMPENSATING', + AMBIGUOUS = 'AMBIGUOUS', + FAILED = 'FAILED', +} + +@Entity('transfer_read_models') +export class TransferReadModelEntity { + @PrimaryColumn('uuid') + sagaId!: string; + + @Column({ type: 'enum', enum: TransferReadStatus }) + status!: TransferReadStatus; + + @Column('uuid') + debitWalletId!: string; + + @Column('uuid') + creditWalletId!: string; + + @Column('bigint') + amount!: string; + + @Column({ length: 3 }) + currency!: string; + + @Column({ type: 'float', nullable: true }) + fxRate!: number | null; + + @Column({ type: 'text', nullable: true }) + receiptId!: string | null; + + @Column({ type: 'text', nullable: true }) + failureReason!: string | null; + + // Versión del último evento proyectado — el cliente puede usar este campo para + // detectar si el read model aún no refleja la saga completa. + @Column({ default: 0 }) + eventVersion!: number; + + @UpdateDateColumn() + updatedAt!: Date; +} diff --git a/apps/query-service/src/read-model/transfer-read-model.module.ts b/apps/query-service/src/read-model/transfer-read-model.module.ts new file mode 100644 index 00000000..093dd5c1 --- /dev/null +++ b/apps/query-service/src/read-model/transfer-read-model.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { TransferReadModelEntity } from './transfer-read-model.entity'; +import { TransferReadModelRepository } from './transfer-read-model.repository'; + +@Module({ + imports: [TypeOrmModule.forFeature([TransferReadModelEntity])], + providers: [TransferReadModelRepository], + exports: [TransferReadModelRepository], +}) +export class TransferReadModelModule {} diff --git a/apps/query-service/src/read-model/transfer-read-model.repository.ts b/apps/query-service/src/read-model/transfer-read-model.repository.ts new file mode 100644 index 00000000..5ccb0764 --- /dev/null +++ b/apps/query-service/src/read-model/transfer-read-model.repository.ts @@ -0,0 +1,20 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { TransferReadModelEntity } from './transfer-read-model.entity'; + +@Injectable() +export class TransferReadModelRepository { + constructor( + @InjectRepository(TransferReadModelEntity) + private readonly repo: Repository, + ) {} + + async upsert(entity: TransferReadModelEntity): Promise { + await this.repo.upsert(entity, ['sagaId']); + } + + async findBySagaId(sagaId: string): Promise { + return this.repo.findOneBy({ sagaId }); + } +} diff --git a/apps/query-service/tsconfig.json b/apps/query-service/tsconfig.json new file mode 100644 index 00000000..f3ed953e --- /dev/null +++ b/apps/query-service/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "./dist", + "baseUrl": "../../" + }, + "include": ["src/**/*", "../../libs/**/*"] +} diff --git a/apps/query-service/webpack.config.js b/apps/query-service/webpack.config.js new file mode 100644 index 00000000..5452e190 --- /dev/null +++ b/apps/query-service/webpack.config.js @@ -0,0 +1,20 @@ +const nodeExternals = require('webpack-node-externals'); +const { TsconfigPathsPlugin } = require('tsconfig-paths-webpack-plugin'); +const path = require('path'); + +module.exports = { + entry: './src/main.ts', + target: 'node', + externals: [nodeExternals({ modulesDir: path.resolve(__dirname, '../../node_modules') })], + resolve: { + extensions: ['.ts', '.js'], + plugins: [new TsconfigPathsPlugin({ configFile: './tsconfig.json' })], + }, + module: { + rules: [{ test: /\.ts$/, use: 'ts-loader', exclude: /node_modules/ }], + }, + output: { + filename: 'main.js', + path: path.resolve(__dirname, 'dist'), + }, +}; diff --git a/apps/receipt-service/Dockerfile b/apps/receipt-service/Dockerfile new file mode 100644 index 00000000..d08a84cf --- /dev/null +++ b/apps/receipt-service/Dockerfile @@ -0,0 +1,9 @@ +FROM wallet-transfer-saga-base AS builder +WORKDIR /app/apps/receipt-service +RUN ../../node_modules/.bin/nest build + +FROM node:22-alpine +WORKDIR /app +COPY --from=builder /app/apps/receipt-service/dist/main.js ./main.js +COPY --from=builder /app/node_modules ./node_modules +CMD ["node", "main.js"] diff --git a/apps/receipt-service/nest-cli.json b/apps/receipt-service/nest-cli.json new file mode 100644 index 00000000..f683d65d --- /dev/null +++ b/apps/receipt-service/nest-cli.json @@ -0,0 +1,9 @@ +{ + "collection": "@nestjs/schematics", + "sourceRoot": "src", + "compilerOptions": { + "webpack": true, + "webpackConfigPath": "webpack.config.js", + "tsConfigPath": "tsconfig.json" + } +} diff --git a/apps/receipt-service/package.json b/apps/receipt-service/package.json new file mode 100644 index 00000000..969330bd --- /dev/null +++ b/apps/receipt-service/package.json @@ -0,0 +1,29 @@ +{ + "name": "@saga/receipt-service", + "version": "1.0.0", + "scripts": { + "build": "nest build", + "start": "node dist/main", + "start:dev": "nest start --watch", + "test": "jest" + }, + "dependencies": { + "@nestjs/common": "^11.1.18", + "@nestjs/core": "^11.1.18", + "@nestjs/microservices": "^11.1.18", + "@nestjs/typeorm": "^11.0.0", + "kafkajs": "^2.2.4", + "typeorm": "^0.3.28", + "pg": "^8.13.0", + "reflect-metadata": "^0.2.2", + "rxjs": "^7.8.1", + "uuid": "^11.0.0" + }, + "devDependencies": { + "@nestjs/cli": "^11.0.0", + "@nestjs/testing": "^11.1.18", + "@types/uuid": "^10.0.0", + "jest": "^29.7.0", + "ts-jest": "^29.2.0" + } +} diff --git a/apps/receipt-service/src/app.module.ts b/apps/receipt-service/src/app.module.ts new file mode 100644 index 00000000..e78d9fb2 --- /dev/null +++ b/apps/receipt-service/src/app.module.ts @@ -0,0 +1,40 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { ClientsModule, Transport } from '@nestjs/microservices'; +import { ReceiptEntity } from './receipt/receipt.entity'; +import { ReceiptModule } from './receipt/receipt.module'; +import { EmitReceiptConsumer } from './consumers/emit-receipt.consumer'; + +@Module({ + imports: [ + TypeOrmModule.forRoot({ + type: 'postgres', + host: process.env.POSTGRES_HOST, + port: parseInt(process.env.POSTGRES_PORT ?? '5432'), + username: process.env.POSTGRES_USER, + password: process.env.POSTGRES_PASSWORD, + database: process.env.POSTGRES_DB, + entities: [ReceiptEntity], + synchronize: process.env.NODE_ENV !== 'production', + }), + ClientsModule.register([ + { + name: 'KAFKA_CLIENT', + transport: Transport.KAFKA, + options: { + client: { + clientId: process.env.KAFKA_CLIENT_ID, + brokers: (process.env.KAFKA_BROKERS ?? '').split(','), + }, + consumer: { + groupId: process.env.KAFKA_CONSUMER_GROUP_RECEIPT ?? 'receipt-service-group', + }, + producerOnlyMode: true, + }, + }, + ]), + ReceiptModule, + ], + controllers: [EmitReceiptConsumer], +}) +export class AppModule {} diff --git a/apps/receipt-service/src/consumers/emit-receipt.consumer.ts b/apps/receipt-service/src/consumers/emit-receipt.consumer.ts new file mode 100644 index 00000000..2c73b25a --- /dev/null +++ b/apps/receipt-service/src/consumers/emit-receipt.consumer.ts @@ -0,0 +1,40 @@ +import { Controller, Inject, Logger, OnModuleInit } from '@nestjs/common'; +import { EventPattern, Payload, ClientKafka } from '@nestjs/microservices'; +import { firstValueFrom } from 'rxjs'; +import { KafkaTopic } from '@contracts/topics'; +import { EmitReceiptCommand } from '@contracts/commands'; +import { ReceiptEmittedEvent } from '@contracts/events'; +import { ReceiptService } from '../receipt/receipt.service'; + +@Controller() +export class EmitReceiptConsumer implements OnModuleInit { + private readonly logger = new Logger(EmitReceiptConsumer.name); + + constructor( + private readonly receiptService: ReceiptService, + @Inject('KAFKA_CLIENT') private readonly kafka: ClientKafka, + ) {} + + async onModuleInit(): Promise { + await this.kafka.connect(); + } + + @EventPattern(KafkaTopic.EMIT_RECEIPT) + async handle(@Payload() cmd: EmitReceiptCommand): Promise { + try { + const { receiptId, alreadyEmitted } = await this.receiptService.emit(cmd); + + if (alreadyEmitted) return; + + const event: ReceiptEmittedEvent = { + sagaId: cmd.sagaId, + receiptId, + emittedAt: new Date().toISOString(), + }; + await firstValueFrom(this.kafka.emit(KafkaTopic.RECEIPT_EMITTED, event)); + } catch (error) { + this.logger.error(`Failed to emit receipt for saga ${cmd.sagaId}`, error); + throw error; // Kafka reintentará el mensaje + } + } +} diff --git a/apps/receipt-service/src/main.ts b/apps/receipt-service/src/main.ts new file mode 100644 index 00000000..29d612ac --- /dev/null +++ b/apps/receipt-service/src/main.ts @@ -0,0 +1,29 @@ +import { NestFactory } from '@nestjs/core'; +import { MicroserviceOptions, Transport } from '@nestjs/microservices'; +import { AppModule } from './app.module'; +import { KafkaExceptionFilter } from '@common/filters/kafka-exception.filter'; + +async function bootstrap(): Promise { + const app = await NestFactory.createMicroservice( + AppModule, + { + transport: Transport.KAFKA, + options: { + client: { + clientId: process.env.KAFKA_CLIENT_ID, + brokers: (process.env.KAFKA_BROKERS ?? '').split(','), + retry: { retries: 10, initialRetryTime: 300 }, + }, + consumer: { + groupId: process.env.KAFKA_CONSUMER_GROUP_RECEIPT ?? 'receipt-service-group', + }, + }, + }, + ); + + app.enableShutdownHooks(); // drain in-flight messages before SIGTERM kills the process + app.useGlobalFilters(new KafkaExceptionFilter()); + await app.listen(); +} + +bootstrap(); diff --git a/apps/receipt-service/src/receipt/receipt.entity.ts b/apps/receipt-service/src/receipt/receipt.entity.ts new file mode 100644 index 00000000..6dfd97ba --- /dev/null +++ b/apps/receipt-service/src/receipt/receipt.entity.ts @@ -0,0 +1,33 @@ +import { + Column, + CreateDateColumn, + Entity, + PrimaryColumn, +} from 'typeorm'; + +@Entity('receipts') +export class ReceiptEntity { + @PrimaryColumn('uuid') + id!: string; + + @Column('uuid') + sagaId!: string; + + @Column('uuid') + debitWalletId!: string; + + @Column('uuid') + creditWalletId!: string; + + @Column('bigint') + amount!: string; + + @Column({ length: 3 }) + currency!: string; // ISO 4217 + + @Column({ type: 'float' }) + fxRate!: number; + + @CreateDateColumn() + createdAt!: Date; +} diff --git a/apps/receipt-service/src/receipt/receipt.module.ts b/apps/receipt-service/src/receipt/receipt.module.ts new file mode 100644 index 00000000..6c9e4291 --- /dev/null +++ b/apps/receipt-service/src/receipt/receipt.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { ReceiptEntity } from './receipt.entity'; +import { ReceiptService } from './receipt.service'; + +@Module({ + imports: [TypeOrmModule.forFeature([ReceiptEntity])], + providers: [ReceiptService], + exports: [ReceiptService], +}) +export class ReceiptModule {} diff --git a/apps/receipt-service/src/receipt/receipt.service.ts b/apps/receipt-service/src/receipt/receipt.service.ts new file mode 100644 index 00000000..53567c9b --- /dev/null +++ b/apps/receipt-service/src/receipt/receipt.service.ts @@ -0,0 +1,45 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { InjectDataSource } from '@nestjs/typeorm'; +import { DataSource } from 'typeorm'; +import { ReceiptEntity } from './receipt.entity'; +import { generateSagaId } from '@helpers/saga-id.helper'; +import { EmitReceiptCommand } from '@contracts/commands'; + +export interface EmitReceiptResult { + receiptId: string; + alreadyEmitted: boolean; +} + +@Injectable() +export class ReceiptService { + private readonly logger = new Logger(ReceiptService.name); + + constructor(@InjectDataSource() private readonly dataSource: DataSource) {} + + // Un receipt por saga — la unicidad se garantiza por sagaId, no por receiptId. + async emit(cmd: EmitReceiptCommand): Promise { + return this.dataSource.transaction(async (manager) => { + const existing = await manager + .getRepository(ReceiptEntity) + .findOneBy({ sagaId: cmd.sagaId }); + + if (existing) { + this.logger.log(`Receipt for saga ${cmd.sagaId} already emitted — skipping`); + return { receiptId: existing.id, alreadyEmitted: true }; + } + + const receipt = new ReceiptEntity(); + receipt.id = generateSagaId(); + receipt.sagaId = cmd.sagaId; + receipt.debitWalletId = cmd.debitWalletId; + receipt.creditWalletId = cmd.creditWalletId; + receipt.amount = cmd.amount.toString(); + receipt.currency = cmd.currency; + receipt.fxRate = cmd.fxRate; + + await manager.save(ReceiptEntity, receipt); + this.logger.log(`Receipt ${receipt.id} emitted for saga ${cmd.sagaId}`); + return { receiptId: receipt.id, alreadyEmitted: false }; + }); + } +} diff --git a/apps/receipt-service/tsconfig.json b/apps/receipt-service/tsconfig.json new file mode 100644 index 00000000..f3ed953e --- /dev/null +++ b/apps/receipt-service/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "./dist", + "baseUrl": "../../" + }, + "include": ["src/**/*", "../../libs/**/*"] +} diff --git a/apps/receipt-service/webpack.config.js b/apps/receipt-service/webpack.config.js new file mode 100644 index 00000000..5452e190 --- /dev/null +++ b/apps/receipt-service/webpack.config.js @@ -0,0 +1,20 @@ +const nodeExternals = require('webpack-node-externals'); +const { TsconfigPathsPlugin } = require('tsconfig-paths-webpack-plugin'); +const path = require('path'); + +module.exports = { + entry: './src/main.ts', + target: 'node', + externals: [nodeExternals({ modulesDir: path.resolve(__dirname, '../../node_modules') })], + resolve: { + extensions: ['.ts', '.js'], + plugins: [new TsconfigPathsPlugin({ configFile: './tsconfig.json' })], + }, + module: { + rules: [{ test: /\.ts$/, use: 'ts-loader', exclude: /node_modules/ }], + }, + output: { + filename: 'main.js', + path: path.resolve(__dirname, 'dist'), + }, +}; diff --git a/apps/wallet-service/Dockerfile b/apps/wallet-service/Dockerfile new file mode 100644 index 00000000..8a4c993f --- /dev/null +++ b/apps/wallet-service/Dockerfile @@ -0,0 +1,9 @@ +FROM wallet-transfer-saga-base AS builder +WORKDIR /app/apps/wallet-service +RUN ../../node_modules/.bin/nest build + +FROM node:22-alpine +WORKDIR /app +COPY --from=builder /app/apps/wallet-service/dist/main.js ./main.js +COPY --from=builder /app/node_modules ./node_modules +CMD ["node", "main.js"] diff --git a/apps/wallet-service/nest-cli.json b/apps/wallet-service/nest-cli.json new file mode 100644 index 00000000..f683d65d --- /dev/null +++ b/apps/wallet-service/nest-cli.json @@ -0,0 +1,9 @@ +{ + "collection": "@nestjs/schematics", + "sourceRoot": "src", + "compilerOptions": { + "webpack": true, + "webpackConfigPath": "webpack.config.js", + "tsConfigPath": "tsconfig.json" + } +} diff --git a/apps/wallet-service/package.json b/apps/wallet-service/package.json new file mode 100644 index 00000000..f10ae82f --- /dev/null +++ b/apps/wallet-service/package.json @@ -0,0 +1,29 @@ +{ + "name": "@saga/wallet-service", + "version": "1.0.0", + "scripts": { + "build": "nest build", + "start": "node dist/main", + "start:dev": "nest start --watch", + "test": "jest" + }, + "dependencies": { + "@nestjs/common": "^11.1.18", + "@nestjs/core": "^11.1.18", + "@nestjs/microservices": "^11.1.18", + "@nestjs/typeorm": "^11.0.0", + "kafkajs": "^2.2.4", + "typeorm": "^0.3.28", + "pg": "^8.13.0", + "reflect-metadata": "^0.2.2", + "rxjs": "^7.8.1", + "uuid": "^11.0.0" + }, + "devDependencies": { + "@nestjs/cli": "^11.0.0", + "@nestjs/testing": "^11.1.18", + "@types/uuid": "^10.0.0", + "jest": "^29.7.0", + "ts-jest": "^29.2.0" + } +} diff --git a/apps/wallet-service/src/app.module.ts b/apps/wallet-service/src/app.module.ts new file mode 100644 index 00000000..313d44d5 --- /dev/null +++ b/apps/wallet-service/src/app.module.ts @@ -0,0 +1,45 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { ClientsModule, Transport } from '@nestjs/microservices'; +import { WalletEntity } from './wallet/wallet.entity'; +import { WalletOperationEntity } from './wallet/wallet-operation.entity'; +import { WalletModule } from './wallet/wallet.module'; +import { WalletEventProducer } from './producers/wallet-event.producer'; +import { DebitWalletConsumer } from './consumers/debit-wallet.consumer'; +import { CreditWalletConsumer } from './consumers/credit-wallet.consumer'; +import { ReverseDebitConsumer } from './consumers/reverse-debit.consumer'; + +@Module({ + imports: [ + TypeOrmModule.forRoot({ + type: 'postgres', + host: process.env.POSTGRES_HOST, + port: parseInt(process.env.POSTGRES_PORT ?? '5432'), + username: process.env.POSTGRES_USER, + password: process.env.POSTGRES_PASSWORD, + database: process.env.POSTGRES_DB, + entities: [WalletEntity, WalletOperationEntity], + synchronize: process.env.NODE_ENV !== 'production', + }), + ClientsModule.register([ + { + name: 'KAFKA_CLIENT', + transport: Transport.KAFKA, + options: { + client: { + clientId: process.env.KAFKA_CLIENT_ID, + brokers: (process.env.KAFKA_BROKERS ?? '').split(','), + }, + consumer: { + groupId: process.env.KAFKA_CONSUMER_GROUP_WALLET ?? 'wallet-service-group', + }, + producerOnlyMode: true, + }, + }, + ]), + WalletModule, + ], + controllers: [DebitWalletConsumer, CreditWalletConsumer, ReverseDebitConsumer], + providers: [WalletEventProducer], +}) +export class AppModule {} diff --git a/apps/wallet-service/src/consumers/credit-wallet.consumer.ts b/apps/wallet-service/src/consumers/credit-wallet.consumer.ts new file mode 100644 index 00000000..2b2a32c2 --- /dev/null +++ b/apps/wallet-service/src/consumers/credit-wallet.consumer.ts @@ -0,0 +1,40 @@ +import { Controller, Logger } from '@nestjs/common'; +import { EventPattern, Payload } from '@nestjs/microservices'; +import { KafkaTopic } from '@contracts/topics'; +import { CreditWalletCommand } from '@contracts/commands'; +import { WalletService } from '../wallet/wallet.service'; +import { WalletEventProducer } from '../producers/wallet-event.producer'; + +@Controller() +export class CreditWalletConsumer { + private readonly logger = new Logger(CreditWalletConsumer.name); + + constructor( + private readonly walletService: WalletService, + private readonly producer: WalletEventProducer, + ) {} + + @EventPattern(KafkaTopic.CREDIT_WALLET) + async handle(@Payload() cmd: CreditWalletCommand): Promise { + try { + await this.walletService.credit({ + sagaId: cmd.sagaId, + walletId: cmd.walletId, + amount: BigInt(cmd.amount), + }); + await this.producer.emitCreditSucceeded({ + sagaId: cmd.sagaId, + walletId: cmd.walletId, + amount: cmd.amount, + balanceAfter: '0', + }); + } catch (error) { + this.logger.error(`Credit failed for saga ${cmd.sagaId}`, error); + await this.producer.emitCreditFailed({ + sagaId: cmd.sagaId, + walletId: cmd.walletId, + reason: error instanceof Error ? error.message : 'UNKNOWN', + }); + } + } +} diff --git a/apps/wallet-service/src/consumers/debit-wallet.consumer.ts b/apps/wallet-service/src/consumers/debit-wallet.consumer.ts new file mode 100644 index 00000000..b22be3c4 --- /dev/null +++ b/apps/wallet-service/src/consumers/debit-wallet.consumer.ts @@ -0,0 +1,40 @@ +import { Controller, Logger } from '@nestjs/common'; +import { EventPattern, Payload } from '@nestjs/microservices'; +import { KafkaTopic } from '@contracts/topics'; +import { DebitWalletCommand } from '@contracts/commands'; +import { WalletService } from '../wallet/wallet.service'; +import { WalletEventProducer } from '../producers/wallet-event.producer'; + +@Controller() +export class DebitWalletConsumer { + private readonly logger = new Logger(DebitWalletConsumer.name); + + constructor( + private readonly walletService: WalletService, + private readonly producer: WalletEventProducer, + ) {} + + @EventPattern(KafkaTopic.DEBIT_WALLET) + async handle(@Payload() cmd: DebitWalletCommand): Promise { + try { + await this.walletService.debit({ + sagaId: cmd.sagaId, + walletId: cmd.walletId, + amount: BigInt(cmd.amount), + }); + await this.producer.emitDebitSucceeded({ + sagaId: cmd.sagaId, + walletId: cmd.walletId, + amount: cmd.amount, + balanceAfter: '0', // el balance real lo tiene la DB — este campo es informativo + }); + } catch (error) { + this.logger.error(`Debit failed for saga ${cmd.sagaId}`, error); + await this.producer.emitDebitFailed({ + sagaId: cmd.sagaId, + walletId: cmd.walletId, + reason: error instanceof Error ? error.message : 'UNKNOWN', + }); + } + } +} diff --git a/apps/wallet-service/src/consumers/reverse-debit.consumer.ts b/apps/wallet-service/src/consumers/reverse-debit.consumer.ts new file mode 100644 index 00000000..e496729c --- /dev/null +++ b/apps/wallet-service/src/consumers/reverse-debit.consumer.ts @@ -0,0 +1,28 @@ +import { Controller, Logger } from '@nestjs/common'; +import { EventPattern, Payload } from '@nestjs/microservices'; +import { KafkaTopic } from '@contracts/topics'; +import { ReverseDebitCommand } from '@contracts/commands'; +import { WalletService } from '../wallet/wallet.service'; + +@Controller() +export class ReverseDebitConsumer { + private readonly logger = new Logger(ReverseDebitConsumer.name); + + constructor(private readonly walletService: WalletService) {} + + @EventPattern(KafkaTopic.REVERSE_DEBIT) + async handle(@Payload() cmd: ReverseDebitCommand): Promise { + try { + await this.walletService.reverseDebit({ + sagaId: cmd.sagaId, + walletId: cmd.walletId, + amount: BigInt(cmd.amount), + }); + this.logger.log(`ReverseDebit completed for saga ${cmd.sagaId} — reason: ${cmd.reason}`); + } catch (error) { + // ReverseDebit fallido es crítico — el balance quedó inconsistente. + // Se loguea como error severo para alerting. No hay compensación de la compensación. + this.logger.error(`ReverseDebit FAILED for saga ${cmd.sagaId} — MANUAL INTERVENTION REQUIRED`, error); + } + } +} diff --git a/apps/wallet-service/src/main.ts b/apps/wallet-service/src/main.ts new file mode 100644 index 00000000..016c98fd --- /dev/null +++ b/apps/wallet-service/src/main.ts @@ -0,0 +1,29 @@ +import { NestFactory } from '@nestjs/core'; +import { MicroserviceOptions, Transport } from '@nestjs/microservices'; +import { AppModule } from './app.module'; +import { KafkaExceptionFilter } from '@common/filters/kafka-exception.filter'; + +async function bootstrap(): Promise { + const app = await NestFactory.createMicroservice( + AppModule, + { + transport: Transport.KAFKA, + options: { + client: { + clientId: process.env.KAFKA_CLIENT_ID, + brokers: (process.env.KAFKA_BROKERS ?? '').split(','), + retry: { retries: 10, initialRetryTime: 300 }, + }, + consumer: { + groupId: process.env.KAFKA_CONSUMER_GROUP_WALLET ?? 'wallet-service-group', + }, + }, + }, + ); + + app.enableShutdownHooks(); // drain in-flight messages before SIGTERM kills the process + app.useGlobalFilters(new KafkaExceptionFilter()); + await app.listen(); +} + +bootstrap(); diff --git a/apps/wallet-service/src/producers/wallet-event.producer.ts b/apps/wallet-service/src/producers/wallet-event.producer.ts new file mode 100644 index 00000000..adbed3ba --- /dev/null +++ b/apps/wallet-service/src/producers/wallet-event.producer.ts @@ -0,0 +1,35 @@ +import { Inject, Injectable, OnModuleInit } from '@nestjs/common'; +import { ClientKafka } from '@nestjs/microservices'; +import { firstValueFrom } from 'rxjs'; +import { KafkaTopic } from '@contracts/topics'; +import { + DebitSucceededEvent, + DebitFailedEvent, + CreditSucceededEvent, + CreditFailedEvent, +} from '@contracts/events'; + +@Injectable() +export class WalletEventProducer implements OnModuleInit { + constructor(@Inject('KAFKA_CLIENT') private readonly kafka: ClientKafka) {} + + async onModuleInit(): Promise { + await this.kafka.connect(); + } + + async emitDebitSucceeded(event: DebitSucceededEvent): Promise { + await firstValueFrom(this.kafka.emit(KafkaTopic.DEBIT_SUCCEEDED, event)); + } + + async emitDebitFailed(event: DebitFailedEvent): Promise { + await firstValueFrom(this.kafka.emit(KafkaTopic.DEBIT_FAILED, event)); + } + + async emitCreditSucceeded(event: CreditSucceededEvent): Promise { + await firstValueFrom(this.kafka.emit(KafkaTopic.CREDIT_SUCCEEDED, event)); + } + + async emitCreditFailed(event: CreditFailedEvent): Promise { + await firstValueFrom(this.kafka.emit(KafkaTopic.CREDIT_FAILED, event)); + } +} diff --git a/apps/wallet-service/src/wallet/wallet-operation.entity.ts b/apps/wallet-service/src/wallet/wallet-operation.entity.ts new file mode 100644 index 00000000..8a6f2017 --- /dev/null +++ b/apps/wallet-service/src/wallet/wallet-operation.entity.ts @@ -0,0 +1,25 @@ +import { Column, CreateDateColumn, Entity, PrimaryColumn } from 'typeorm'; + +export enum OperationType { + DEBIT = 'DEBIT', + CREDIT = 'CREDIT', + REVERSE_DEBIT = 'REVERSE_DEBIT', +} + +@Entity('wallet_operations') +export class WalletOperationEntity { + @PrimaryColumn() + idempotencyKey!: string; + + @Column('uuid') + walletId!: string; + + @Column({ type: 'enum', enum: OperationType }) + type!: OperationType; + + @Column('bigint') + amount!: string; + + @CreateDateColumn() + createdAt!: Date; +} diff --git a/apps/wallet-service/src/wallet/wallet.entity.ts b/apps/wallet-service/src/wallet/wallet.entity.ts new file mode 100644 index 00000000..7b4191b3 --- /dev/null +++ b/apps/wallet-service/src/wallet/wallet.entity.ts @@ -0,0 +1,29 @@ +import { + Column, + CreateDateColumn, + Entity, + PrimaryColumn, + UpdateDateColumn, + VersionColumn, +} from 'typeorm'; + +@Entity('wallets') +export class WalletEntity { + @PrimaryColumn('uuid') + id!: string; + + @Column('bigint') + balance!: string; + + @Column({ length: 3 }) + currency!: string; // ISO 4217 + + @VersionColumn() + version!: number; + + @CreateDateColumn() + createdAt!: Date; + + @UpdateDateColumn() + updatedAt!: Date; +} diff --git a/apps/wallet-service/src/wallet/wallet.module.ts b/apps/wallet-service/src/wallet/wallet.module.ts new file mode 100644 index 00000000..bc502c75 --- /dev/null +++ b/apps/wallet-service/src/wallet/wallet.module.ts @@ -0,0 +1,13 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { WalletEntity } from './wallet.entity'; +import { WalletOperationEntity } from './wallet-operation.entity'; +import { WalletRepository } from './wallet.repository'; +import { WalletService } from './wallet.service'; + +@Module({ + imports: [TypeOrmModule.forFeature([WalletEntity, WalletOperationEntity])], + providers: [WalletRepository, WalletService], + exports: [WalletService], +}) +export class WalletModule {} diff --git a/apps/wallet-service/src/wallet/wallet.repository.ts b/apps/wallet-service/src/wallet/wallet.repository.ts new file mode 100644 index 00000000..f57271b7 --- /dev/null +++ b/apps/wallet-service/src/wallet/wallet.repository.ts @@ -0,0 +1,37 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { EntityManager, Repository } from 'typeorm'; +import { WalletEntity } from './wallet.entity'; + +@Injectable() +export class WalletRepository { + constructor( + @InjectRepository(WalletEntity) + private readonly repo: Repository, + ) {} + + // SELECT FOR UPDATE dentro de una transacción — bloquea la fila hasta el commit. + // Garantiza que dos débitos simultáneos sobre el mismo walletId no lean el mismo balance. + async findByIdForUpdate( + walletId: string, + manager: EntityManager, + ): Promise { + return manager + .getRepository(WalletEntity) + .createQueryBuilder('wallet') + .setLock('pessimistic_write') + .where('wallet.id = :walletId', { walletId }) + .getOne(); + } + + async findById(walletId: string): Promise { + return this.repo.findOneBy({ id: walletId }); + } + + async saveWithManager( + wallet: WalletEntity, + manager: EntityManager, + ): Promise { + return manager.save(WalletEntity, wallet); + } +} diff --git a/apps/wallet-service/src/wallet/wallet.service.ts b/apps/wallet-service/src/wallet/wallet.service.ts new file mode 100644 index 00000000..3c535956 --- /dev/null +++ b/apps/wallet-service/src/wallet/wallet.service.ts @@ -0,0 +1,79 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { InjectDataSource } from '@nestjs/typeorm'; +import { DataSource } from 'typeorm'; +import { WalletRepository } from './wallet.repository'; +import { WalletOperationEntity, OperationType } from './wallet-operation.entity'; +import { subtractMoney, addMoney } from '@helpers/money.helper'; +import { buildIdempotencyKey } from '@helpers/idempotency-key.helper'; + +interface WalletOperationParams { + sagaId: string; + walletId: string; + amount: bigint; +} + +@Injectable() +export class WalletService { + private readonly logger = new Logger(WalletService.name); + + constructor( + @InjectDataSource() private readonly dataSource: DataSource, + private readonly walletRepo: WalletRepository, + ) {} + + async debit(params: WalletOperationParams): Promise { + const idempotencyKey = buildIdempotencyKey(params.sagaId, 'debit'); + await this.executeOperation(idempotencyKey, params, OperationType.DEBIT); + } + + async credit(params: WalletOperationParams): Promise { + const idempotencyKey = buildIdempotencyKey(params.sagaId, 'credit'); + await this.executeOperation(idempotencyKey, params, OperationType.CREDIT); + } + + async reverseDebit(params: WalletOperationParams): Promise { + const idempotencyKey = buildIdempotencyKey(params.sagaId, 'reverse-debit'); + await this.executeOperation(idempotencyKey, params, OperationType.REVERSE_DEBIT); + } + + // Toda la operación ocurre en una única transacción local: + // 1. Verifica idempotencia (INSERT de la operación — falla si ya existe) + // 2. Adquiere lock pesimista sobre el wallet (SELECT FOR UPDATE) + // 3. Valida y actualiza el balance + // Si cualquier paso falla, la transacción entera hace rollback — ACID garantizado. + private async executeOperation( + idempotencyKey: string, + params: WalletOperationParams, + type: OperationType, + ): Promise { + await this.dataSource.transaction(async (manager) => { + const alreadyProcessed = await manager.findOneBy(WalletOperationEntity, { + idempotencyKey, + }); + if (alreadyProcessed) { + this.logger.log(`Operation ${idempotencyKey} already processed — skipping`); + return; + } + + const wallet = await this.walletRepo.findByIdForUpdate(params.walletId, manager); + if (!wallet) throw new Error(`Wallet ${params.walletId} not found`); + + const currentBalance = BigInt(wallet.balance); + + if (type === OperationType.DEBIT) { + wallet.balance = subtractMoney(currentBalance, params.amount).toString(); + } else { + wallet.balance = addMoney(currentBalance, params.amount).toString(); + } + + const operation = new WalletOperationEntity(); + operation.idempotencyKey = idempotencyKey; + operation.walletId = params.walletId; + operation.type = type; + operation.amount = params.amount.toString(); + + await this.walletRepo.saveWithManager(wallet, manager); + await manager.save(WalletOperationEntity, operation); + }); + } +} diff --git a/apps/wallet-service/test/wallet.service.spec.ts b/apps/wallet-service/test/wallet.service.spec.ts new file mode 100644 index 00000000..75820b01 --- /dev/null +++ b/apps/wallet-service/test/wallet.service.spec.ts @@ -0,0 +1,151 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { WalletService } from '../src/wallet/wallet.service'; +import { WalletRepository } from '../src/wallet/wallet.repository'; +import { WalletEntity } from '../src/wallet/wallet.entity'; +import { WalletOperationEntity } from '../src/wallet/wallet-operation.entity'; +import { DataSource, EntityManager } from 'typeorm'; + +const buildWallet = (balance: string): WalletEntity => { + const w = new WalletEntity(); + w.id = 'wallet-001'; + w.balance = balance; + w.currency = 'PEN'; + w.version = 1; + return w; +}; + +const buildManager = (_wallet: WalletEntity | null, existingOp: WalletOperationEntity | null = null) => ({ + findOneBy: jest.fn().mockResolvedValue(existingOp), + save: jest.fn().mockResolvedValue(undefined), + getRepository: jest.fn(), +}); + +const buildDataSource = (wallet: WalletEntity | null, existingOp: WalletOperationEntity | null = null) => { + const manager = buildManager(wallet, existingOp); + return { + transaction: jest.fn().mockImplementation((cb: (m: EntityManager) => Promise) => + cb(manager as unknown as EntityManager), + ), + _manager: manager, + }; +}; + +const buildWalletRepo = (wallet: WalletEntity | null) => ({ + findByIdForUpdate: jest.fn().mockResolvedValue(wallet), + saveWithManager: jest.fn().mockResolvedValue(undefined), + findById: jest.fn(), +}); + +describe('WalletService', () => { + let service: WalletService; + let walletRepo: ReturnType; + let dataSource: ReturnType; + + const setupModule = async (wallet: WalletEntity | null, existingOp: WalletOperationEntity | null = null) => { + walletRepo = buildWalletRepo(wallet); + dataSource = buildDataSource(wallet, existingOp); + + const module: TestingModule = await Test.createTestingModule({ + providers: [ + WalletService, + { provide: WalletRepository, useValue: walletRepo }, + { provide: DataSource, useValue: dataSource }, + ], + }).compile(); + + service = module.get(WalletService); + }; + + describe('debit', () => { + it('descuenta el monto del balance', async () => { + await setupModule(buildWallet('10000')); + + await service.debit({ sagaId: 'saga-001', walletId: 'wallet-001', amount: 3000n }); + + expect(walletRepo.saveWithManager).toHaveBeenCalledWith( + expect.objectContaining({ balance: '7000' }), + expect.anything(), + ); + }); + + it('lanza INSUFFICIENT_FUNDS si el monto supera el balance', async () => { + await setupModule(buildWallet('1000')); + + await expect( + service.debit({ sagaId: 'saga-001', walletId: 'wallet-001', amount: 5000n }), + ).rejects.toThrow('INSUFFICIENT_FUNDS'); + }); + + it('nunca permite balance negativo', async () => { + await setupModule(buildWallet('0')); + + await expect( + service.debit({ sagaId: 'saga-001', walletId: 'wallet-001', amount: 1n }), + ).rejects.toThrow('INSUFFICIENT_FUNDS'); + }); + + it('es idempotente — no modifica el balance si la operación ya fue procesada', async () => { + const existingOp = new WalletOperationEntity(); + existingOp.idempotencyKey = 'saga-001::debit'; + await setupModule(buildWallet('10000'), existingOp); + + await service.debit({ sagaId: 'saga-001', walletId: 'wallet-001', amount: 3000n }); + + expect(walletRepo.saveWithManager).not.toHaveBeenCalled(); + }); + + it('lanza error si el wallet no existe', async () => { + await setupModule(null); + + await expect( + service.debit({ sagaId: 'saga-001', walletId: 'inexistente', amount: 100n }), + ).rejects.toThrow('Wallet inexistente not found'); + }); + }); + + describe('credit', () => { + it('suma el monto al balance', async () => { + await setupModule(buildWallet('5000')); + + await service.credit({ sagaId: 'saga-001', walletId: 'wallet-001', amount: 2000n }); + + expect(walletRepo.saveWithManager).toHaveBeenCalledWith( + expect.objectContaining({ balance: '7000' }), + expect.anything(), + ); + }); + + it('es idempotente', async () => { + const existingOp = new WalletOperationEntity(); + existingOp.idempotencyKey = 'saga-001::credit'; + await setupModule(buildWallet('5000'), existingOp); + + await service.credit({ sagaId: 'saga-001', walletId: 'wallet-001', amount: 2000n }); + + expect(walletRepo.saveWithManager).not.toHaveBeenCalled(); + }); + }); + + describe('reverseDebit', () => { + it('restaura el balance correctamente', async () => { + await setupModule(buildWallet('7000')); + + await service.reverseDebit({ sagaId: 'saga-001', walletId: 'wallet-001', amount: 3000n }); + + expect(walletRepo.saveWithManager).toHaveBeenCalledWith( + expect.objectContaining({ balance: '10000' }), + expect.anything(), + ); + }); + + it('es idempotente', async () => { + const existingOp = new WalletOperationEntity(); + existingOp.idempotencyKey = 'saga-001::reverse-debit'; + await setupModule(buildWallet('7000'), existingOp); + + await service.reverseDebit({ sagaId: 'saga-001', walletId: 'wallet-001', amount: 3000n }); + + expect(walletRepo.saveWithManager).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/apps/wallet-service/tsconfig.json b/apps/wallet-service/tsconfig.json new file mode 100644 index 00000000..f3ed953e --- /dev/null +++ b/apps/wallet-service/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "./dist", + "baseUrl": "../../" + }, + "include": ["src/**/*", "../../libs/**/*"] +} diff --git a/apps/wallet-service/webpack.config.js b/apps/wallet-service/webpack.config.js new file mode 100644 index 00000000..5452e190 --- /dev/null +++ b/apps/wallet-service/webpack.config.js @@ -0,0 +1,20 @@ +const nodeExternals = require('webpack-node-externals'); +const { TsconfigPathsPlugin } = require('tsconfig-paths-webpack-plugin'); +const path = require('path'); + +module.exports = { + entry: './src/main.ts', + target: 'node', + externals: [nodeExternals({ modulesDir: path.resolve(__dirname, '../../node_modules') })], + resolve: { + extensions: ['.ts', '.js'], + plugins: [new TsconfigPathsPlugin({ configFile: './tsconfig.json' })], + }, + module: { + rules: [{ test: /\.ts$/, use: 'ts-loader', exclude: /node_modules/ }], + }, + output: { + filename: 'main.js', + path: path.resolve(__dirname, 'dist'), + }, +}; diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..cd5c8f07 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,223 @@ +networks: + saga-net: + driver: bridge + +volumes: + postgres_data: + +services: + zookeeper: + image: confluentinc/cp-zookeeper:7.6.0 + hostname: zookeeper + networks: [saga-net] + environment: + ZOOKEEPER_CLIENT_PORT: ${ZOOKEEPER_PORT} + ZOOKEEPER_TICK_TIME: 2000 + healthcheck: + test: ["CMD", "nc", "-z", "localhost", "${ZOOKEEPER_PORT}"] + interval: 10s + timeout: 5s + retries: 5 + + kafka: + image: confluentinc/cp-kafka:7.6.0 + hostname: kafka + networks: [saga-net] + ports: + - "${KAFKA_EXTERNAL_PORT}:${KAFKA_EXTERNAL_PORT}" + depends_on: + zookeeper: + condition: service_healthy + environment: + KAFKA_BROKER_ID: 1 + KAFKA_ZOOKEEPER_CONNECT: zookeeper:${ZOOKEEPER_PORT} + KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT + KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:${KAFKA_INTERNAL_PORT},PLAINTEXT_HOST://localhost:${KAFKA_EXTERNAL_PORT} + KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT + KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 + KAFKA_AUTO_CREATE_TOPICS_ENABLE: "true" + healthcheck: + test: ["CMD", "kafka-broker-api-versions", "--bootstrap-server", "localhost:${KAFKA_EXTERNAL_PORT}"] + interval: 15s + timeout: 10s + retries: 10 + + postgres: + image: postgres:17 + hostname: postgres + networks: [saga-net] + ports: + - "${POSTGRES_PORT}:${POSTGRES_PORT}" + volumes: + - postgres_data:/var/lib/postgresql/data + environment: + POSTGRES_USER: ${POSTGRES_USER} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + POSTGRES_DB: ${POSTGRES_DB} + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"] + interval: 10s + timeout: 5s + retries: 5 + + fx-mock: + image: node:22-alpine + hostname: fx-mock + networks: [saga-net] + ports: + - "${FX_MOCK_PORT}:${FX_MOCK_PORT}" + environment: + FX_MOCK_PORT: ${FX_MOCK_PORT} + FX_MOCK_DEFAULT_DELAY_MS: ${FX_MOCK_DEFAULT_DELAY_MS} + FX_MOCK_DEFAULT_RATE: ${FX_MOCK_DEFAULT_RATE} + command: > + node -e " + const http = require('http'); + const PORT = parseInt(process.env.FX_MOCK_PORT); + const DEFAULT_DELAY = parseInt(process.env.FX_MOCK_DEFAULT_DELAY_MS); + const DEFAULT_RATE = parseFloat(process.env.FX_MOCK_DEFAULT_RATE); + http.createServer((req, res) => { + const delay = parseInt(new URL(req.url, 'http://x').searchParams.get('delay') || DEFAULT_DELAY); + setTimeout(() => { + res.writeHead(200, {'Content-Type': 'application/json'}); + res.end(JSON.stringify({ rate: DEFAULT_RATE, settled: true })); + }, delay); + }).listen(PORT, () => console.log('fx-mock on :' + PORT)); + " + healthcheck: + test: ["CMD-SHELL", "wget -qO- http://localhost:${FX_MOCK_PORT}/ || exit 0"] + interval: 10s + timeout: 5s + retries: 3 + + kafka-init: + image: confluentinc/cp-kafka:7.6.0 + networks: [saga-net] + depends_on: + kafka: + condition: service_healthy + entrypoint: ["/bin/bash", "-c"] + command: > + "kafka-topics --bootstrap-server kafka:${KAFKA_INTERNAL_PORT} --create --if-not-exists --topic wallet.debit.cmd --partitions 1 --replication-factor 1 && + kafka-topics --bootstrap-server kafka:${KAFKA_INTERNAL_PORT} --create --if-not-exists --topic wallet.credit.cmd --partitions 1 --replication-factor 1 && + kafka-topics --bootstrap-server kafka:${KAFKA_INTERNAL_PORT} --create --if-not-exists --topic wallet.reverse-debit.cmd --partitions 1 --replication-factor 1 && + kafka-topics --bootstrap-server kafka:${KAFKA_INTERNAL_PORT} --create --if-not-exists --topic fx.settle.cmd --partitions 1 --replication-factor 1 && + kafka-topics --bootstrap-server kafka:${KAFKA_INTERNAL_PORT} --create --if-not-exists --topic receipt.emit.cmd --partitions 1 --replication-factor 1 && + kafka-topics --bootstrap-server kafka:${KAFKA_INTERNAL_PORT} --create --if-not-exists --topic wallet.debit.succeeded --partitions 1 --replication-factor 1 && + kafka-topics --bootstrap-server kafka:${KAFKA_INTERNAL_PORT} --create --if-not-exists --topic wallet.debit.failed --partitions 1 --replication-factor 1 && + kafka-topics --bootstrap-server kafka:${KAFKA_INTERNAL_PORT} --create --if-not-exists --topic wallet.credit.succeeded --partitions 1 --replication-factor 1 && + kafka-topics --bootstrap-server kafka:${KAFKA_INTERNAL_PORT} --create --if-not-exists --topic wallet.credit.failed --partitions 1 --replication-factor 1 && + kafka-topics --bootstrap-server kafka:${KAFKA_INTERNAL_PORT} --create --if-not-exists --topic fx.settled --partitions 1 --replication-factor 1 && + kafka-topics --bootstrap-server kafka:${KAFKA_INTERNAL_PORT} --create --if-not-exists --topic fx.failed --partitions 1 --replication-factor 1 && + kafka-topics --bootstrap-server kafka:${KAFKA_INTERNAL_PORT} --create --if-not-exists --topic fx.ambiguous --partitions 1 --replication-factor 1 && + kafka-topics --bootstrap-server kafka:${KAFKA_INTERNAL_PORT} --create --if-not-exists --topic receipt.emitted --partitions 1 --replication-factor 1 && + kafka-topics --bootstrap-server kafka:${KAFKA_INTERNAL_PORT} --create --if-not-exists --topic transfer.updated --partitions 1 --replication-factor 1 && + echo 'All topics created'" + restart: "no" + + db-seed: + image: postgres:17 + networks: [saga-net] + env_file: .env.example + environment: + PGPASSWORD: ${POSTGRES_PASSWORD} + depends_on: + postgres: + condition: service_healthy + entrypoint: ["/bin/bash", "-c"] + command: > + "until psql -h postgres -U ${POSTGRES_USER} -d ${POSTGRES_DB} -c 'SELECT 1 FROM wallets LIMIT 1' > /dev/null 2>&1; do + echo 'Waiting for wallets table...'; sleep 2; + done && + psql -h postgres -U ${POSTGRES_USER} -d ${POSTGRES_DB} -c \" + INSERT INTO wallets (id, balance, currency, version, \\\"createdAt\\\", \\\"updatedAt\\\") + VALUES + ('a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11', 10000, 'PEN', 1, NOW(), NOW()), + ('b0eebc99-9c0b-4ef8-bb6d-6bb9bd380a22', 5000, 'PEN', 1, NOW(), NOW()) + ON CONFLICT (id) DO NOTHING; + \" && echo 'Seed complete'" + restart: "no" + + orchestrator: + build: + context: . + dockerfile: apps/orchestrator/Dockerfile + hostname: orchestrator + networks: [saga-net] + ports: + - "${ORCHESTRATOR_PORT}:${ORCHESTRATOR_PORT}" + env_file: .env.example + environment: + KAFKA_BROKERS: kafka:${KAFKA_INTERNAL_PORT} + POSTGRES_HOST: postgres + depends_on: + kafka-init: + condition: service_completed_successfully + postgres: + condition: service_healthy + + wallet-service: + build: + context: . + dockerfile: apps/wallet-service/Dockerfile + hostname: wallet-service + networks: [saga-net] + env_file: .env.example + environment: + KAFKA_BROKERS: kafka:${KAFKA_INTERNAL_PORT} + POSTGRES_HOST: postgres + depends_on: + kafka-init: + condition: service_completed_successfully + postgres: + condition: service_healthy + + fx-service: + build: + context: . + dockerfile: apps/fx-service/Dockerfile + hostname: fx-service + networks: [saga-net] + env_file: .env.example + environment: + KAFKA_BROKERS: kafka:${KAFKA_INTERNAL_PORT} + FX_API_URL: http://fx-mock:${FX_MOCK_PORT} + depends_on: + kafka-init: + condition: service_completed_successfully + fx-mock: + condition: service_started + + receipt-service: + build: + context: . + dockerfile: apps/receipt-service/Dockerfile + hostname: receipt-service + networks: [saga-net] + env_file: .env.example + environment: + KAFKA_BROKERS: kafka:${KAFKA_INTERNAL_PORT} + POSTGRES_HOST: postgres + depends_on: + kafka-init: + condition: service_completed_successfully + postgres: + condition: service_healthy + + query-service: + build: + context: . + dockerfile: apps/query-service/Dockerfile + hostname: query-service + networks: [saga-net] + ports: + - "${QUERY_SERVICE_PORT}:${QUERY_SERVICE_PORT}" + env_file: .env.example + environment: + KAFKA_BROKERS: kafka:${KAFKA_INTERNAL_PORT} + POSTGRES_HOST: postgres + depends_on: + kafka-init: + condition: service_completed_successfully + postgres: + condition: service_healthy diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 00000000..879b1839 --- /dev/null +++ b/jest.config.js @@ -0,0 +1,17 @@ +/** @type {import('jest').Config} */ +module.exports = { + moduleFileExtensions: ['js', 'json', 'ts'], + rootDir: '.', + testRegex: '.*\\.spec\\.ts$', + transform: { + '^.+\\.(t|j)s$': 'ts-jest', + }, + collectCoverageFrom: ['apps/**/src/**/*.ts', 'libs/**/*.ts'], + coverageDirectory: './coverage', + testEnvironment: 'node', + moduleNameMapper: { + '^@contracts/(.*)$': '/libs/contracts/$1', + '^@common/(.*)$': '/libs/common/$1', + '^@helpers/(.*)$': '/libs/helpers/$1', + }, +}; diff --git a/libs/common/filters/kafka-exception.filter.ts b/libs/common/filters/kafka-exception.filter.ts new file mode 100644 index 00000000..c69c6ebf --- /dev/null +++ b/libs/common/filters/kafka-exception.filter.ts @@ -0,0 +1,17 @@ +import { Catch, Logger } from '@nestjs/common'; +import { BaseRpcExceptionFilter, RpcException } from '@nestjs/microservices'; +import { Observable, throwError } from 'rxjs'; + +@Catch() +export class KafkaExceptionFilter extends BaseRpcExceptionFilter { + private readonly logger = new Logger(KafkaExceptionFilter.name); + + catch(exception: unknown): Observable { + this.logger.error('Unhandled Kafka exception', exception); + const rpcException = + exception instanceof RpcException + ? exception + : new RpcException(String(exception)); + return throwError(() => rpcException); + } +} diff --git a/libs/common/interceptors/logging.interceptor.ts b/libs/common/interceptors/logging.interceptor.ts new file mode 100644 index 00000000..51cfb513 --- /dev/null +++ b/libs/common/interceptors/logging.interceptor.ts @@ -0,0 +1,23 @@ +import { + CallHandler, + ExecutionContext, + Injectable, + Logger, + NestInterceptor, +} from '@nestjs/common'; +import { Observable, tap } from 'rxjs'; + +@Injectable() +export class LoggingInterceptor implements NestInterceptor { + private readonly logger = new Logger(LoggingInterceptor.name); + + intercept(context: ExecutionContext, next: CallHandler): Observable { + const req = context.switchToHttp().getRequest<{ method: string; url: string }>(); + const { method, url } = req; + const start = Date.now(); + + return next.handle().pipe( + tap(() => this.logger.log(`${method} ${url} — ${Date.now() - start}ms`)), + ); + } +} diff --git a/libs/common/interfaces/compensable.interface.ts b/libs/common/interfaces/compensable.interface.ts new file mode 100644 index 00000000..60ef77d5 --- /dev/null +++ b/libs/common/interfaces/compensable.interface.ts @@ -0,0 +1,4 @@ +// No todos los pasos son reversibles — forzar compensate() en todos rompería FX y Receipt. +export interface ICompensable { + compensate(command: TCommand): Promise; +} diff --git a/libs/common/interfaces/saga-step.interface.ts b/libs/common/interfaces/saga-step.interface.ts new file mode 100644 index 00000000..e7fed2d7 --- /dev/null +++ b/libs/common/interfaces/saga-step.interface.ts @@ -0,0 +1,4 @@ +// Kafka puede redeliverar mensajes — execute() debe tolerar duplicados sin efecto secundario. +export interface ISagaStep { + execute(command: TCommand): Promise; +} diff --git a/libs/contracts/commands/credit-wallet.command.ts b/libs/contracts/commands/credit-wallet.command.ts new file mode 100644 index 00000000..9cff805e --- /dev/null +++ b/libs/contracts/commands/credit-wallet.command.ts @@ -0,0 +1,6 @@ +export interface CreditWalletCommand { + sagaId: string; + walletId: string; + amount: string; + currency: string; +} diff --git a/libs/contracts/commands/debit-wallet.command.ts b/libs/contracts/commands/debit-wallet.command.ts new file mode 100644 index 00000000..54a78fd2 --- /dev/null +++ b/libs/contracts/commands/debit-wallet.command.ts @@ -0,0 +1,6 @@ +export interface DebitWalletCommand { + sagaId: string; // idempotency key del paso + walletId: string; + amount: string; // centavos (ver money.helper) + currency: string; // ISO 4217 +} diff --git a/libs/contracts/commands/emit-receipt.command.ts b/libs/contracts/commands/emit-receipt.command.ts new file mode 100644 index 00000000..151a8e39 --- /dev/null +++ b/libs/contracts/commands/emit-receipt.command.ts @@ -0,0 +1,8 @@ +export interface EmitReceiptCommand { + sagaId: string; + debitWalletId: string; + creditWalletId: string; + amount: string; + currency: string; + fxRate: number; +} diff --git a/libs/contracts/commands/index.ts b/libs/contracts/commands/index.ts new file mode 100644 index 00000000..efd82a47 --- /dev/null +++ b/libs/contracts/commands/index.ts @@ -0,0 +1,5 @@ +export * from './debit-wallet.command'; +export * from './credit-wallet.command'; +export * from './reverse-debit.command'; +export * from './settle-fx.command'; +export * from './emit-receipt.command'; diff --git a/libs/contracts/commands/reverse-debit.command.ts b/libs/contracts/commands/reverse-debit.command.ts new file mode 100644 index 00000000..e82a583b --- /dev/null +++ b/libs/contracts/commands/reverse-debit.command.ts @@ -0,0 +1,7 @@ +export interface ReverseDebitCommand { + sagaId: string; + walletId: string; + amount: string; + currency: string; + reason: string; // requerido para auditoría +} diff --git a/libs/contracts/commands/settle-fx.command.ts b/libs/contracts/commands/settle-fx.command.ts new file mode 100644 index 00000000..453785c6 --- /dev/null +++ b/libs/contracts/commands/settle-fx.command.ts @@ -0,0 +1,6 @@ +export interface SettleFxCommand { + sagaId: string; + fromCurrency: string; + toCurrency: string; + amount: string; +} diff --git a/libs/contracts/events/credit-failed.event.ts b/libs/contracts/events/credit-failed.event.ts new file mode 100644 index 00000000..be365e29 --- /dev/null +++ b/libs/contracts/events/credit-failed.event.ts @@ -0,0 +1,5 @@ +export interface CreditFailedEvent { + sagaId: string; + walletId: string; + reason: string; +} diff --git a/libs/contracts/events/credit-succeeded.event.ts b/libs/contracts/events/credit-succeeded.event.ts new file mode 100644 index 00000000..3a4bb56b --- /dev/null +++ b/libs/contracts/events/credit-succeeded.event.ts @@ -0,0 +1,6 @@ +export interface CreditSucceededEvent { + sagaId: string; + walletId: string; + amount: string; + balanceAfter: string; +} diff --git a/libs/contracts/events/debit-failed.event.ts b/libs/contracts/events/debit-failed.event.ts new file mode 100644 index 00000000..88974c86 --- /dev/null +++ b/libs/contracts/events/debit-failed.event.ts @@ -0,0 +1,5 @@ +export interface DebitFailedEvent { + sagaId: string; + walletId: string; + reason: string; // 'INSUFFICIENT_FUNDS' | 'WALLET_LOCKED' | 'CONFLICT' +} diff --git a/libs/contracts/events/debit-succeeded.event.ts b/libs/contracts/events/debit-succeeded.event.ts new file mode 100644 index 00000000..f4d2172c --- /dev/null +++ b/libs/contracts/events/debit-succeeded.event.ts @@ -0,0 +1,6 @@ +export interface DebitSucceededEvent { + sagaId: string; + walletId: string; + amount: string; + balanceAfter: string; +} diff --git a/libs/contracts/events/fx-ambiguous.event.ts b/libs/contracts/events/fx-ambiguous.event.ts new file mode 100644 index 00000000..5f63e0bb --- /dev/null +++ b/libs/contracts/events/fx-ambiguous.event.ts @@ -0,0 +1,7 @@ +// FX no respondió dentro de FX_TIMEOUT_MS. Estado terminal-pendiente: +// requiere reconciliación manual — el orquestador no reintenta automáticamente. +export interface FxAmbiguousEvent { + sagaId: string; + timeoutMs: number; + attemptedAt: string; // ISO 8601 +} diff --git a/libs/contracts/events/fx-failed.event.ts b/libs/contracts/events/fx-failed.event.ts new file mode 100644 index 00000000..f39ecdcd --- /dev/null +++ b/libs/contracts/events/fx-failed.event.ts @@ -0,0 +1,4 @@ +export interface FxFailedEvent { + sagaId: string; + reason: string; +} diff --git a/libs/contracts/events/fx-settled.event.ts b/libs/contracts/events/fx-settled.event.ts new file mode 100644 index 00000000..612f055e --- /dev/null +++ b/libs/contracts/events/fx-settled.event.ts @@ -0,0 +1,7 @@ +export interface FxSettledEvent { + sagaId: string; + fromCurrency: string; + toCurrency: string; + rate: number; + settledAt: string; // ISO 8601 +} diff --git a/libs/contracts/events/index.ts b/libs/contracts/events/index.ts new file mode 100644 index 00000000..f982f9ec --- /dev/null +++ b/libs/contracts/events/index.ts @@ -0,0 +1,9 @@ +export * from './debit-succeeded.event'; +export * from './debit-failed.event'; +export * from './credit-succeeded.event'; +export * from './credit-failed.event'; +export * from './fx-settled.event'; +export * from './fx-failed.event'; +export * from './fx-ambiguous.event'; +export * from './receipt-emitted.event'; +export * from './transfer-updated.event'; diff --git a/libs/contracts/events/receipt-emitted.event.ts b/libs/contracts/events/receipt-emitted.event.ts new file mode 100644 index 00000000..e2998fff --- /dev/null +++ b/libs/contracts/events/receipt-emitted.event.ts @@ -0,0 +1,5 @@ +export interface ReceiptEmittedEvent { + sagaId: string; + receiptId: string; + emittedAt: string; // ISO 8601 +} diff --git a/libs/contracts/events/transfer-updated.event.ts b/libs/contracts/events/transfer-updated.event.ts new file mode 100644 index 00000000..2ae4f318 --- /dev/null +++ b/libs/contracts/events/transfer-updated.event.ts @@ -0,0 +1,11 @@ +export interface TransferUpdatedEvent { + sagaId: string; + state: string; + debitWalletId: string; + creditWalletId: string; + amount: string; + currency: string; + fxRate: number | null; + receiptId: string | null; + failureReason: string | null; +} diff --git a/libs/contracts/topics.ts b/libs/contracts/topics.ts new file mode 100644 index 00000000..14a0b4ec --- /dev/null +++ b/libs/contracts/topics.ts @@ -0,0 +1,22 @@ +// Único source of truth para topic names. No usar strings directos en consumers/producers. +export enum KafkaTopic { + // Commands: orchestrator → services + DEBIT_WALLET = 'wallet.debit.cmd', + CREDIT_WALLET = 'wallet.credit.cmd', + REVERSE_DEBIT = 'wallet.reverse-debit.cmd', + SETTLE_FX = 'fx.settle.cmd', + EMIT_RECEIPT = 'receipt.emit.cmd', + + // Events: services → orchestrator + DEBIT_SUCCEEDED = 'wallet.debit.succeeded', + DEBIT_FAILED = 'wallet.debit.failed', + CREDIT_SUCCEEDED = 'wallet.credit.succeeded', + CREDIT_FAILED = 'wallet.credit.failed', + FX_SETTLED = 'fx.settled', + FX_FAILED = 'fx.failed', + FX_AMBIGUOUS = 'fx.ambiguous', + RECEIPT_EMITTED = 'receipt.emitted', + + // Projections: services → query-service + TRANSFER_UPDATED = 'transfer.updated', +} diff --git a/libs/helpers/idempotency-key.helper.ts b/libs/helpers/idempotency-key.helper.ts new file mode 100644 index 00000000..e39965ec --- /dev/null +++ b/libs/helpers/idempotency-key.helper.ts @@ -0,0 +1,4 @@ +// Derivar del request permitiría colisiones si el cliente reintenta con datos distintos. +export function buildIdempotencyKey(sagaId: string, step: string): string { + return `${sagaId}::${step}`; +} diff --git a/libs/helpers/money.helper.spec.ts b/libs/helpers/money.helper.spec.ts new file mode 100644 index 00000000..775545bb --- /dev/null +++ b/libs/helpers/money.helper.spec.ts @@ -0,0 +1,65 @@ +import { toCents, fromCents, addMoney, subtractMoney, isPositive } from './money.helper'; + +describe('money.helper', () => { + describe('toCents', () => { + it('convierte decimal a centavos', () => { + expect(toCents(10.5)).toBe(1050n); + }); + + it('redondea correctamente valores con más de 2 decimales', () => { + expect(toCents(10.005)).toBe(1001n); + }); + + it('maneja enteros', () => { + expect(toCents(100)).toBe(10000n); + }); + }); + + describe('fromCents', () => { + it('convierte centavos a decimal', () => { + expect(fromCents(1050n)).toBe(10.5); + }); + + it('es la inversa de toCents para valores estándar', () => { + expect(fromCents(toCents(25.99))).toBe(25.99); + }); + }); + + describe('addMoney', () => { + it('suma correctamente', () => { + expect(addMoney(1000n, 500n)).toBe(1500n); + }); + + it('suma cero sin cambio', () => { + expect(addMoney(1000n, 0n)).toBe(1000n); + }); + }); + + describe('subtractMoney', () => { + it('resta correctamente', () => { + expect(subtractMoney(1000n, 300n)).toBe(700n); + }); + + it('lanza INSUFFICIENT_FUNDS cuando el monto supera el balance', () => { + expect(() => subtractMoney(500n, 600n)).toThrow('INSUFFICIENT_FUNDS'); + }); + + it('lanza INSUFFICIENT_FUNDS con balance cero', () => { + expect(() => subtractMoney(0n, 1n)).toThrow('INSUFFICIENT_FUNDS'); + }); + + it('permite restar el balance completo', () => { + expect(subtractMoney(1000n, 1000n)).toBe(0n); + }); + }); + + describe('isPositive', () => { + it('retorna true para montos positivos', () => { + expect(isPositive(1n)).toBe(true); + }); + + it('retorna false para cero', () => { + expect(isPositive(0n)).toBe(false); + }); + }); +}); diff --git a/libs/helpers/money.helper.ts b/libs/helpers/money.helper.ts new file mode 100644 index 00000000..edbc58df --- /dev/null +++ b/libs/helpers/money.helper.ts @@ -0,0 +1,25 @@ +// Aritmética monetaria en bigint (centavos). $10.50 → 1050n +// MONEY_SUBUNIT_FACTOR controla la subdivisión — defecto 100 (centavos). + +const SUBUNIT_FACTOR = BigInt(process.env.MONEY_SUBUNIT_FACTOR ?? '100'); + +export function toCents(amount: number): bigint { + return BigInt(Math.round(amount * Number(SUBUNIT_FACTOR))); +} + +export function fromCents(cents: bigint): number { + return Number(cents) / Number(SUBUNIT_FACTOR); +} + +export function addMoney(a: bigint, b: bigint): bigint { + return a + b; +} + +export function subtractMoney(a: bigint, b: bigint): bigint { + if (b > a) throw new Error('INSUFFICIENT_FUNDS'); + return a - b; +} + +export function isPositive(amount: bigint): boolean { + return amount > 0n; +} diff --git a/libs/helpers/saga-id.helper.ts b/libs/helpers/saga-id.helper.ts new file mode 100644 index 00000000..cccbc63e --- /dev/null +++ b/libs/helpers/saga-id.helper.ts @@ -0,0 +1,5 @@ +import { v4 as uuidv4 } from 'uuid'; + +export function generateSagaId(): string { + return uuidv4(); +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..f34ba222 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,8518 @@ +{ + "name": "wallet-transfer-saga", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "wallet-transfer-saga", + "workspaces": [ + "apps/*", + "libs/*" + ], + "dependencies": { + "@nestjs/common": "^11.1.18", + "@nestjs/core": "^11.1.18", + "@nestjs/microservices": "^11.1.18", + "@nestjs/typeorm": "^11.0.1", + "class-transformer": "^0.5.1", + "class-validator": "^0.15.1", + "reflect-metadata": "^0.2.2", + "rxjs": "^7.8.2", + "tsconfig-paths": "^4.2.0", + "typeorm": "^0.3.28", + "uuid": "^13.0.0" + }, + "devDependencies": { + "@nestjs/cli": "^11.0.0", + "@nestjs/testing": "^11.1.18", + "@types/jest": "^29.5.14", + "@types/node": "^22.0.0", + "jest": "^29.7.0", + "ts-jest": "^29.4.9", + "ts-loader": "^9.5.7", + "typescript": "^5.4.5" + } + }, + "apps/fx-service": { + "name": "@saga/fx-service", + "version": "1.0.0", + "dependencies": { + "@nestjs/common": "^11.1.18", + "@nestjs/core": "^11.1.18", + "@nestjs/microservices": "^11.1.18", + "kafkajs": "^2.2.4", + "reflect-metadata": "^0.2.2", + "rxjs": "^7.8.1", + "uuid": "^11.0.0" + }, + "devDependencies": { + "@nestjs/cli": "^11.0.0", + "@nestjs/testing": "^11.1.18", + "@types/uuid": "^10.0.0", + "jest": "^29.7.0", + "ts-jest": "^29.2.0" + } + }, + "apps/fx-service/node_modules/uuid": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, + "apps/orchestrator": { + "name": "@saga/orchestrator", + "version": "1.0.0", + "dependencies": { + "@nestjs/common": "^11.1.18", + "@nestjs/core": "^11.1.18", + "@nestjs/microservices": "^11.1.18", + "@nestjs/platform-express": "^11.1.18", + "@nestjs/typeorm": "^11.0.0", + "kafkajs": "^2.2.4", + "pg": "^8.13.0", + "reflect-metadata": "^0.2.2", + "rxjs": "^7.8.1", + "typeorm": "^0.3.28", + "uuid": "^11.0.0" + }, + "devDependencies": { + "@nestjs/cli": "^11.0.0", + "@nestjs/testing": "^11.1.18", + "@types/uuid": "^10.0.0", + "jest": "^29.7.0", + "ts-jest": "^29.2.0" + } + }, + "apps/orchestrator/node_modules/uuid": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, + "apps/query-service": { + "name": "@saga/query-service", + "version": "1.0.0", + "dependencies": { + "@nestjs/common": "^11.1.18", + "@nestjs/core": "^11.1.18", + "@nestjs/microservices": "^11.1.18", + "@nestjs/platform-express": "^11.1.18", + "@nestjs/typeorm": "^11.0.0", + "kafkajs": "^2.2.4", + "pg": "^8.13.0", + "reflect-metadata": "^0.2.2", + "rxjs": "^7.8.1", + "typeorm": "^0.3.28", + "uuid": "^11.0.0" + }, + "devDependencies": { + "@nestjs/cli": "^11.0.0", + "@nestjs/testing": "^11.1.18", + "@types/uuid": "^10.0.0", + "jest": "^29.7.0", + "ts-jest": "^29.2.0" + } + }, + "apps/query-service/node_modules/uuid": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, + "apps/receipt-service": { + "name": "@saga/receipt-service", + "version": "1.0.0", + "dependencies": { + "@nestjs/common": "^11.1.18", + "@nestjs/core": "^11.1.18", + "@nestjs/microservices": "^11.1.18", + "@nestjs/typeorm": "^11.0.0", + "kafkajs": "^2.2.4", + "pg": "^8.13.0", + "reflect-metadata": "^0.2.2", + "rxjs": "^7.8.1", + "typeorm": "^0.3.28", + "uuid": "^11.0.0" + }, + "devDependencies": { + "@nestjs/cli": "^11.0.0", + "@nestjs/testing": "^11.1.18", + "@types/uuid": "^10.0.0", + "jest": "^29.7.0", + "ts-jest": "^29.2.0" + } + }, + "apps/receipt-service/node_modules/uuid": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, + "apps/wallet-service": { + "name": "@saga/wallet-service", + "version": "1.0.0", + "dependencies": { + "@nestjs/common": "^11.1.18", + "@nestjs/core": "^11.1.18", + "@nestjs/microservices": "^11.1.18", + "@nestjs/typeorm": "^11.0.0", + "kafkajs": "^2.2.4", + "pg": "^8.13.0", + "reflect-metadata": "^0.2.2", + "rxjs": "^7.8.1", + "typeorm": "^0.3.28", + "uuid": "^11.0.0" + }, + "devDependencies": { + "@nestjs/cli": "^11.0.0", + "@nestjs/testing": "^11.1.18", + "@types/uuid": "^10.0.0", + "jest": "^29.7.0", + "ts-jest": "^29.2.0" + } + }, + "apps/wallet-service/node_modules/uuid": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, + "node_modules/@angular-devkit/core": { + "version": "19.2.24", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-19.2.24.tgz", + "integrity": "sha512-Kd49warf6U/EyWe5BszF/eebN3zQ3bk7tgfEljAw8q/rX95UUtriJubWvp6pgzHfzBA4jwq8f+QiNZB8eBEXPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "8.18.0", + "ajv-formats": "3.0.1", + "jsonc-parser": "3.3.1", + "picomatch": "4.0.4", + "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/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@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/core/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/@angular-devkit/schematics": { + "version": "19.2.24", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-19.2.24.tgz", + "integrity": "sha512-lnw+ZM1Io+cJAkReC0NPDjqObL8NtKzKIkdgEEKC8CUmkhurYhedbicN8Y8NYHgG1uLd2GozW3+/QqPRZaN+Lw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "19.2.24", + "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.24", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics-cli/-/schematics-cli-19.2.24.tgz", + "integrity": "sha512-bsStZQG67J1HBqTmWxtIcobvgrn32L4UOdL7hGyOru5VxDWPNA8pRnDYavT3hnJeBkJYPoQIw8u7Dm0ecoQprw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "19.2.24", + "@angular-devkit/schematics": "19.2.24", + "@inquirer/prompts": "7.3.2", + "ansi-colors": "4.1.3", + "symbol-observable": "4.0.0", + "yargs-parser": "21.1.1" + }, + "bin": { + "schematics": "bin/schematics.js" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular-devkit/schematics-cli/node_modules/@inquirer/prompts": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.3.2.tgz", + "integrity": "sha512-G1ytyOoHh5BphmEBxSwALin3n1KGNYB6yImbICcRQdzXfOGbuJ9Jske/Of5Sebk339NSGGNfUshnzK8YWkTPsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/checkbox": "^4.1.2", + "@inquirer/confirm": "^5.1.6", + "@inquirer/editor": "^4.2.7", + "@inquirer/expand": "^4.0.9", + "@inquirer/input": "^4.1.6", + "@inquirer/number": "^3.0.9", + "@inquirer/password": "^4.0.9", + "@inquirer/rawlist": "^4.0.9", + "@inquirer/search": "^3.0.9", + "@inquirer/select": "^4.0.9" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/schematics/node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.28.6.tgz", + "integrity": "sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.28.6.tgz", + "integrity": "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.28.6.tgz", + "integrity": "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@borewit/text-codec": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@borewit/text-codec/-/text-codec-0.2.2.tgz", + "integrity": "sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@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/core/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==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@inquirer/core/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/@inquirer/editor": { + "version": "4.2.23", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.23.tgz", + "integrity": "sha512-aLSROkEwirotxZ1pBaP8tugXRFCxW94gwrQLxXfrZsKkfjOYC1aRvAZuhpJOb5cu4IBTJdsCigUlf2iCOu4ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/external-editor": "^1.0.3", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/expand": { + "version": "4.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.23.tgz", + "integrity": "sha512-nRzdOyFYnpeYTTR2qFwEVmIWypzdAx/sIkCMeTNTcflFOovfqUk+HcFhQQVBftAh9gmGrpFj6QcGEqrDMDOiew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/external-editor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.3.tgz", + "integrity": "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chardet": "^2.1.1", + "iconv-lite": "^0.7.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/figures": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.15.tgz", + "integrity": "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/input": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.3.1.tgz", + "integrity": "sha512-kN0pAM4yPrLjJ1XJBjDxyfDduXOuQHrBB8aLDMueuwUGn+vNpF7Gq7TvyVxx8u4SHlFFj4trmj+a2cbpG4Jn1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/number": { + "version": "3.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.23.tgz", + "integrity": "sha512-5Smv0OK7K0KUzUfYUXDXQc9jrf8OHo4ktlEayFlelCjwMXz0299Y8OrI+lj7i4gCBY15UObk76q0QtxjzFcFcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/password": { + "version": "4.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.23.tgz", + "integrity": "sha512-zREJHjhT5vJBMZX/IUbyI9zVtVfOLiTO66MrF/3GFZYZ7T4YILW5MSkEYHceSii/KtRk+4i3RE7E1CUXA2jHcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/prompts": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.10.1.tgz", + "integrity": "sha512-Dx/y9bCQcXLI5ooQ5KyvA4FTgeo2jYj/7plWfV5Ak5wDPKQZgudKez2ixyfz7tKXzcJciTxqLeK7R9HItwiByg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/checkbox": "^4.3.2", + "@inquirer/confirm": "^5.1.21", + "@inquirer/editor": "^4.2.23", + "@inquirer/expand": "^4.0.23", + "@inquirer/input": "^4.3.1", + "@inquirer/number": "^3.0.23", + "@inquirer/password": "^4.0.23", + "@inquirer/rawlist": "^4.1.11", + "@inquirer/search": "^3.2.2", + "@inquirer/select": "^4.4.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/rawlist": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.1.11.tgz", + "integrity": "sha512-+LLQB8XGr3I5LZN/GuAHo+GpDJegQwuPARLChlMICNdwW7OwV2izlCSCxN6cqpL0sMXmbKbFcItJgdQq5EBXTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/search": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.2.2.tgz", + "integrity": "sha512-p2bvRfENXCZdWF/U2BXvnSI9h+tuA8iNqtUKb9UWbmLYCRQxd8WkvwWvYn+3NgYaNwdUkHytJMGG4MMLucI1kA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/select": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.4.2.tgz", + "integrity": "sha512-l4xMuJo55MAe+N7Qr4rX90vypFwCajSakx59qe/tMaC1aEHWLyw68wF4o0A4SLAY4E0nd+Vt+EyskeDIqu1M6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/type": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.10.tgz", + "integrity": "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/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": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/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/@nestjs/cli": { + "version": "11.0.19", + "resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-11.0.19.tgz", + "integrity": "sha512-9htODqTVVNH4lJqyeIotsAgfeaYngDi020cVCd6JhJRKuOT83c/t4JDSky6+xr0lhHyNTNMgZmulxqcMNZFfrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "19.2.24", + "@angular-devkit/schematics": "19.2.24", + "@angular-devkit/schematics-cli": "19.2.24", + "@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.6", + "node-emoji": "1.11.0", + "ora": "5.4.1", + "tsconfig-paths": "4.2.0", + "tsconfig-paths-webpack-plugin": "4.2.0", + "typescript": "5.9.3", + "webpack": "5.106.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 || ^0.8.0", + "@swc/core": "^1.3.62" + }, + "peerDependenciesMeta": { + "@swc/cli": { + "optional": true + }, + "@swc/core": { + "optional": true + } + } + }, + "node_modules/@nestjs/cli/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@nestjs/cli/node_modules/brace-expansion": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@nestjs/cli/node_modules/glob": { + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@nestjs/cli/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@nestjs/common": { + "version": "11.1.18", + "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.1.18.tgz", + "integrity": "sha512-0sLq8Z+TIjLnz1Tqp0C/x9BpLbqpt1qEu0VcH4/fkE0y3F5JxhfK1AdKQ/SPbKhKgwqVDoY4gS8GQr2G6ujaWg==", + "license": "MIT", + "dependencies": { + "file-type": "21.3.4", + "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/core": { + "version": "11.1.18", + "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-11.1.18.tgz", + "integrity": "sha512-wR3DtGyk/LUAiPtbXDuWJJwVkWElKBY0sqnTzf9d4uM3+X18FRZhK7WFc47czsIGOdWuRsMeLYV+1Z9dO4zDEQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@nuxt/opencollective": "0.4.1", + "fast-safe-stringify": "2.1.1", + "iterare": "1.2.1", + "path-to-regexp": "8.4.2", + "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/microservices": { + "version": "11.1.18", + "resolved": "https://registry.npmjs.org/@nestjs/microservices/-/microservices-11.1.18.tgz", + "integrity": "sha512-sodzBqqKAcOv5GctTbb0j3vzgx+X3/IxmJ8pBKL9hEmsarsYPkig6dcOcVFmM2Pknm+o6Cxaqv1YZZETISNi+w==", + "license": "MIT", + "dependencies": { + "iterare": "1.2.1", + "tslib": "2.8.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "@grpc/grpc-js": "*", + "@nestjs/common": "^11.0.0", + "@nestjs/core": "^11.0.0", + "@nestjs/websockets": "^11.0.0", + "amqp-connection-manager": "*", + "amqplib": "*", + "cache-manager": "*", + "ioredis": "*", + "kafkajs": "*", + "mqtt": "*", + "nats": "*", + "reflect-metadata": "^0.1.12 || ^0.2.0", + "rxjs": "^7.1.0" + }, + "peerDependenciesMeta": { + "@grpc/grpc-js": { + "optional": true + }, + "@nestjs/websockets": { + "optional": true + }, + "amqp-connection-manager": { + "optional": true + }, + "amqplib": { + "optional": true + }, + "cache-manager": { + "optional": true + }, + "ioredis": { + "optional": true + }, + "kafkajs": { + "optional": true + }, + "mqtt": { + "optional": true + }, + "nats": { + "optional": true + } + } + }, + "node_modules/@nestjs/platform-express": { + "version": "11.1.18", + "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.1.18.tgz", + "integrity": "sha512-s6GdHMTa3qx0fJewR74Xa30ysPHfBEqxIwZ7BGSTLoAEQ1vTP24urNl+b6+s49NFLEIOyeNho5fN/9/I17QlOw==", + "license": "MIT", + "dependencies": { + "cors": "2.8.6", + "express": "5.2.1", + "multer": "2.1.1", + "path-to-regexp": "8.4.2", + "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.10", + "resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-11.0.10.tgz", + "integrity": "sha512-q9lr0wGwgBHLarD4uno3XiW4JX60WPlg2VTgbqPHl/6bT4u1IEEzj+q9Tad3bVnqL5mlDF3vrZ2tj+x13CJpmw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "19.2.23", + "@angular-devkit/schematics": "19.2.23", + "comment-json": "4.6.2", + "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.23", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-19.2.23.tgz", + "integrity": "sha512-RazHPQkUEsNU/OZ75w9UeHxGFMthRiuAW2B/uA7eXExBj/1meHrrBfoCA56ujW2GUxVjRtSrMjylKh4R4meiYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "8.18.0", + "ajv-formats": "3.0.1", + "jsonc-parser": "3.3.1", + "picomatch": "4.0.4", + "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.23", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-19.2.23.tgz", + "integrity": "sha512-Jzs7YM4X6azmHU7Mw5tQSPMuvaqYS8SLnZOJbtiXCy1JyuW9bm/WBBecNHMiuZ8LHXKhvQ6AVX1tKrzF6uiDmw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "19.2.23", + "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/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@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/schematics/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/@nestjs/testing": { + "version": "11.1.18", + "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-11.1.18.tgz", + "integrity": "sha512-frzwNlpBgtAzI3hp/qo57DZoRO4RMTH1wST3QUYEhRTHyfPkLpzkWz3jV/mhApXjD0yT56Ptlzn6zuYPLh87Lw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "@nestjs/common": "^11.0.0", + "@nestjs/core": "^11.0.0", + "@nestjs/microservices": "^11.0.0", + "@nestjs/platform-express": "^11.0.0" + }, + "peerDependenciesMeta": { + "@nestjs/microservices": { + "optional": true + }, + "@nestjs/platform-express": { + "optional": true + } + } + }, + "node_modules/@nestjs/typeorm": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/@nestjs/typeorm/-/typeorm-11.0.1.tgz", + "integrity": "sha512-8rw/nKT0S+L+MkzgE9F2/mox7mAgsPlwfzmW9gsESN1lmQtIrVEfiiBwC2O8+guS1jBfQehJIdcdUj2OAp4VUQ==", + "license": "MIT", + "peerDependencies": { + "@nestjs/common": "^10.0.0 || ^11.0.0", + "@nestjs/core": "^10.0.0 || ^11.0.0", + "reflect-metadata": "^0.1.13 || ^0.2.0", + "rxjs": "^7.2.0", + "typeorm": "^0.3.0 || ^1.0.0-dev" + } + }, + "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/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@saga/fx-service": { + "resolved": "apps/fx-service", + "link": true + }, + "node_modules/@saga/orchestrator": { + "resolved": "apps/orchestrator", + "link": true + }, + "node_modules/@saga/query-service": { + "resolved": "apps/query-service", + "link": true + }, + "node_modules/@saga/receipt-service": { + "resolved": "apps/receipt-service", + "link": true + }, + "node_modules/@saga/wallet-service": { + "resolved": "apps/wallet-service", + "link": true + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.10", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz", + "integrity": "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@sqltools/formatter": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@sqltools/formatter/-/formatter-1.2.5.tgz", + "integrity": "sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw==", + "license": "MIT" + }, + "node_modules/@tokenizer/inflate": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.4.1.tgz", + "integrity": "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "token-types": "^6.1.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@tokenizer/token": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", + "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", + "license": "MIT" + }, + "node_modules/@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/eslint": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "29.5.14", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.14.tgz", + "integrity": "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, + "node_modules/@types/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/node": { + "version": "22.19.17", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.17.tgz", + "integrity": "sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "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/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/@webassemblyjs/ast": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-import-phases": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", + "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + }, + "peerDependencies": { + "acorn": "^8.14.0" + } + }, + "node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "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-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/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ansis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/ansis/-/ansis-4.2.0.tgz", + "integrity": "sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==", + "license": "ISC", + "engines": { + "node": ">=14" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/app-root-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/app-root-path/-/app-root-path-3.1.0.tgz", + "integrity": "sha512-biN3PwB2gUtjaYy/isrU3aNWI5w+fAfvHkSvCKeQGxhmYpwKFUxudR3Yya+KqVRHBmEDYh+/lTozYCFbmzX4nA==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", + "license": "MIT" + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/array-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/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", + "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.18", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.18.tgz", + "integrity": "sha512-VSnGQAOLtP5mib/DPyg2/t+Tlv65NTBz83BJBJvmLVHHuKJVaDOBvJJykiT5TR++em5nfAySPccDZDa4oSrn8A==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "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.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-json-stable-stringify": "2.x" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer": { + "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-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", + "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "get-intrinsic": "^1.3.0", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001787", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001787.tgz", + "integrity": "sha512-mNcrMN9KeI68u7muanUpEejSLghOKlVhRqS/Za2IeyGllJ9I9otGpR9g3nsw7n4W378TE/LyIteA0+/FOZm4Kg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/chardet": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.1.tgz", + "integrity": "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/chrome-trace-event": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", + "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/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" + }, + "node_modules/class-validator": { + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.15.1.tgz", + "integrity": "sha512-LqoS80HBBSCVhz/3KloUly0ovokxpdOLR++Al3J3+dHXWt9sTKlKd4eYtoxhxyUjoe5+UcIM+5k9MIxyBWnRTw==", + "license": "MIT", + "dependencies": { + "@types/validator": "^13.15.3", + "libphonenumber-js": "^1.11.1", + "validator": "^13.15.22" + } + }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-table3": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.5.tgz", + "integrity": "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "string-width": "^4.2.0" + }, + "engines": { + "node": "10.* || >= 12.*" + }, + "optionalDependencies": { + "@colors/colors": "1.5.0" + } + }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz", + "integrity": "sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==", + "dev": true, + "license": "MIT" + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/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.6.2", + "resolved": "https://registry.npmjs.org/comment-json/-/comment-json-4.6.2.tgz", + "integrity": "sha512-R2rze/hDX30uul4NZoIZ76ImSJLFxn/1/ZxtKC1L77y2X1k+yYu1joKbAtMA2Fg3hZrTOiw0I5mwVMo0cf250w==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-timsort": "^1.0.3", + "esprima": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "engines": [ + "node >= 6.0" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/content-disposition": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", + "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/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cosmiconfig": { + "version": "8.3.6", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", + "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0", + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/cosmiconfig/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/cosmiconfig/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/create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/dayjs": { + "version": "1.11.20", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.20.tgz", + "integrity": "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/dedent": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.2.tgz", + "integrity": "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==", + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "license": "MIT" + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.335", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.335.tgz", + "integrity": "sha512-q9n5T4BR4Xwa2cwbrwcsDJtHD/enpQ5S1xF1IAtdqf5AAgqDFmR/aakqH3ChFdqd/QXJhS3rnnXFtexU7rax6Q==", + "dev": true, + "license": "ISC" + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.20.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz", + "integrity": "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", + "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "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/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/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/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/esrecurse/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/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/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/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "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/file-type": { + "version": "21.3.4", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-21.3.4.tgz", + "integrity": "sha512-Ievi/yy8DS3ygGvT47PjSfdFoX+2isQueoYP1cntFW1JLYAuS4GD7NUPGg4zv2iZfV52uDyk5w5Z0TdpRS6Q1g==", + "license": "MIT", + "dependencies": { + "@tokenizer/inflate": "^0.4.1", + "strtok3": "^10.3.4", + "token-types": "^6.1.1", + "uint8array-extras": "^1.4.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/find-up": { + "version": "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/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/foreground-child/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/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/fork-ts-checker-webpack-plugin/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/fs-monkey": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.1.0.tgz", + "integrity": "sha512-QMUezzXWII9EV5aTFXW1UBVUO77wYPpjqIF8/AviUCThNeSYZykpoTixUeaNNBwmCev0AMDWMAni+f8Hxb1IFw==", + "dev": true, + "license": "Unlicense" + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-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/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/handlebars": { + "version": "4.7.9", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.9.tgz", + "integrity": "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/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-fresh/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/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-instrument/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/iterare": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/iterare/-/iterare-1.2.1.tgz", + "integrity": "sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q==", + "license": "ISC", + "engines": { + "node": ">=6" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "3.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/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-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/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "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/kafkajs": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/kafkajs/-/kafkajs-2.2.4.tgz", + "integrity": "sha512-j/YeapB1vfPT2iOIUn/vxdyKEuhuY2PxMBvf5JWux6iSaukAccrMtXEY/Lb7OvavDhOWME589bpLrEdnVHjfjA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/libphonenumber-js": { + "version": "1.12.41", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.12.41.tgz", + "integrity": "sha512-lsmMmGXBxXIK/VMLEj0kL6MtUs1kBGj1nTCzi6zgQoG1DEwqwt2DQyHxcLykceIxAnfE3hya7NuIh6PpC6S3fA==", + "license": "MIT" + }, + "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": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "dev": true, + "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/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/magic-string": { + "version": "0.30.17", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", + "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "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/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/multer": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/multer/-/multer-2.1.1.tgz", + "integrity": "sha512-mo+QTzKlx8R7E5ylSXxWzGoXoZbOsRMpyitcht8By2KHvMbf3tjwosZ/Mu/XYU6UuJ3VZnODIrak5ZrPiPyB6A==", + "license": "MIT", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.6.0", + "concat-stream": "^2.0.0", + "type-is": "^1.6.18" + }, + "engines": { + "node": ">= 10.16.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/multer/node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/multer/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/multer/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/multer/node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mute-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", + "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-abort-controller": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz", + "integrity": "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-emoji": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.11.0.tgz", + "integrity": "sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash": "^4.17.21" + } + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.37", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.37.tgz", + "integrity": "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/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": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-locate/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.0" + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "11.3.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.3.tgz", + "integrity": "sha512-JvNw9Y81y33E+BEYPr0U7omo+U9AySnsMsEiXgwT6yqd31VQWTLNQqmT4ou5eqPFUrTfIDFta2wKhB1hyohtAQ==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/path-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pg": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.20.0.tgz", + "integrity": "sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA==", + "license": "MIT", + "dependencies": { + "pg-connection-string": "^2.12.0", + "pg-pool": "^3.13.0", + "pg-protocol": "^1.13.0", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.3.0" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.3.0.tgz", + "integrity": "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==", + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.12.0.tgz", + "integrity": "sha512-U7qg+bpswf3Cs5xLzRqbXbQl85ng0mfSV/J0nnA31MCLgvEaAo7CIhmeyrmJpOr7o+zm0rXK+hNnT5l9RHkCkQ==", + "license": "MIT" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.13.0.tgz", + "integrity": "sha512-gB+R+Xud1gLFuRD/QgOIgGOBE2KCQPaPwkzBBGC9oG69pHTkhQeIuejVIk3/cnDyX39av2AxomQiyPT13WKHQA==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.13.0.tgz", + "integrity": "sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w==", + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "license": "MIT", + "dependencies": { + "split2": "^4.1.0" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pluralize": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", + "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz", + "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/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": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/qs": { + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/reflect-metadata": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", + "license": "Apache-2.0" + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve.exports": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", + "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/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/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/schema-utils/node_modules/ajv": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/schema-utils/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/schema-utils/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/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/sha.js": { + "version": "2.4.12", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.12.tgz", + "integrity": "sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==", + "license": "(MIT AND BSD-3-Clause)", + "dependencies": { + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.0" + }, + "bin": { + "sha.js": "bin.js" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/sql-highlight": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/sql-highlight/-/sql-highlight-6.1.0.tgz", + "integrity": "sha512-ed7OK4e9ywpE7pgRMkMQmZDPKSVdm0oX5IEtZiKnFucSF0zu6c80GZBe38UqHuVhTWJ9xsKgSMjCG2bml86KvA==", + "funding": [ + "https://github.com/scriptcoded/sql-highlight?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/scriptcoded" + } + ], + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strtok3": { + "version": "10.3.5", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.3.5.tgz", + "integrity": "sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA==", + "license": "MIT", + "dependencies": { + "@tokenizer/token": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/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/tapable": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.2.tgz", + "integrity": "sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/terser": { + "version": "5.46.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.1.tgz", + "integrity": "sha512-vzCjQO/rgUuK9sf8VJZvjqiqiHFaZLnOiimmUuOKODxWL8mm/xua7viT7aqX7dgPY60otQjUotzFMmCB4VdmqQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.4.0.tgz", + "integrity": "sha512-Bn5vxm48flOIfkdl5CaD2+1CiUVbonWQ3KQPyP7/EuIl9Gbzq/gQFOzaMFUEgVjB1396tcK0SG8XcNJ/2kDH8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.0", + "terser": "^5.31.1" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/terser-webpack-plugin/node_modules/ajv-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/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/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/terser/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/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/to-buffer": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.2.tgz", + "integrity": "sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==", + "license": "MIT", + "dependencies": { + "isarray": "^2.0.5", + "safe-buffer": "^5.2.1", + "typed-array-buffer": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/token-types": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.1.2.tgz", + "integrity": "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==", + "license": "MIT", + "dependencies": { + "@borewit/text-codec": "^0.2.1", + "@tokenizer/token": "^0.3.0", + "ieee754": "^1.2.1" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/ts-jest": { + "version": "29.4.9", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.9.tgz", + "integrity": "sha512-LTb9496gYPMCqjeDLdPrKuXtncudeV1yRZnF4Wo5l3SFi0RYEnYRNgMrFIdg+FHvfzjCyQk1cLncWVqiSX+EvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bs-logger": "^0.2.6", + "fast-json-stable-stringify": "^2.1.0", + "handlebars": "^4.7.9", + "json5": "^2.2.3", + "lodash.memoize": "^4.1.2", + "make-error": "^1.3.6", + "semver": "^7.7.4", + "type-fest": "^4.41.0", + "yargs-parser": "^21.1.1" + }, + "bin": { + "ts-jest": "cli.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.0 <8", + "@jest/transform": "^29.0.0 || ^30.0.0", + "@jest/types": "^29.0.0 || ^30.0.0", + "babel-jest": "^29.0.0 || ^30.0.0", + "jest": "^29.0.0 || ^30.0.0", + "jest-util": "^29.0.0 || ^30.0.0", + "typescript": ">=4.3 <7" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@jest/transform": { + "optional": true + }, + "@jest/types": { + "optional": true + }, + "babel-jest": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jest-util": { + "optional": true + } + } + }, + "node_modules/ts-jest/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ts-jest/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ts-loader": { + "version": "9.5.7", + "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.5.7.tgz", + "integrity": "sha512-/ZNrKgA3K3PtpMYOC71EeMWIloGw3IYEa5/t1cyz2r5/PyUwTXGzYJvcD3kfUvmhlfpz1rhV8B2O6IVTQ0avsg==", + "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-loader/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ts-loader/node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "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==", + "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==", + "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-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "license": "MIT" + }, + "node_modules/typeorm": { + "version": "0.3.28", + "resolved": "https://registry.npmjs.org/typeorm/-/typeorm-0.3.28.tgz", + "integrity": "sha512-6GH7wXhtfq2D33ZuRXYwIsl/qM5685WZcODZb7noOOcRMteM9KF2x2ap3H0EBjnSV0VO4gNAfJT5Ukp0PkOlvg==", + "license": "MIT", + "dependencies": { + "@sqltools/formatter": "^1.2.5", + "ansis": "^4.2.0", + "app-root-path": "^3.1.0", + "buffer": "^6.0.3", + "dayjs": "^1.11.19", + "debug": "^4.4.3", + "dedent": "^1.7.0", + "dotenv": "^16.6.1", + "glob": "^10.5.0", + "reflect-metadata": "^0.2.2", + "sha.js": "^2.4.12", + "sql-highlight": "^6.1.0", + "tslib": "^2.8.1", + "uuid": "^11.1.0", + "yargs": "^17.7.2" + }, + "bin": { + "typeorm": "cli.js", + "typeorm-ts-node-commonjs": "cli-ts-node-commonjs.js", + "typeorm-ts-node-esm": "cli-ts-node-esm.js" + }, + "engines": { + "node": ">=16.13.0" + }, + "funding": { + "url": "https://opencollective.com/typeorm" + }, + "peerDependencies": { + "@google-cloud/spanner": "^5.18.0 || ^6.0.0 || ^7.0.0 || ^8.0.0", + "@sap/hana-client": "^2.14.22", + "better-sqlite3": "^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 || ^12.0.0", + "ioredis": "^5.0.4", + "mongodb": "^5.8.0 || ^6.0.0", + "mssql": "^9.1.1 || ^10.0.0 || ^11.0.0 || ^12.0.0", + "mysql2": "^2.2.5 || ^3.0.1", + "oracledb": "^6.3.0", + "pg": "^8.5.1", + "pg-native": "^3.0.0", + "pg-query-stream": "^4.0.0", + "redis": "^3.1.1 || ^4.0.0 || ^5.0.14", + "sql.js": "^1.4.0", + "sqlite3": "^5.0.3", + "ts-node": "^10.7.0", + "typeorm-aurora-data-api-driver": "^2.0.0 || ^3.0.0" + }, + "peerDependenciesMeta": { + "@google-cloud/spanner": { + "optional": true + }, + "@sap/hana-client": { + "optional": true + }, + "better-sqlite3": { + "optional": true + }, + "ioredis": { + "optional": true + }, + "mongodb": { + "optional": true + }, + "mssql": { + "optional": true + }, + "mysql2": { + "optional": true + }, + "oracledb": { + "optional": true + }, + "pg": { + "optional": true + }, + "pg-native": { + "optional": true + }, + "pg-query-stream": { + "optional": true + }, + "redis": { + "optional": true + }, + "sql.js": { + "optional": true + }, + "sqlite3": { + "optional": true + }, + "ts-node": { + "optional": true + }, + "typeorm-aurora-data-api-driver": { + "optional": true + } + } + }, + "node_modules/typeorm/node_modules/brace-expansion": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/typeorm/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/typeorm/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/typeorm/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, + "node_modules/typeorm/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/typeorm/node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/typeorm/node_modules/uuid": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/uglify-js": { + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/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/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/uuid": { + "version": "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-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/validator": { + "version": "13.15.35", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.35.tgz", + "integrity": "sha512-TQ5pAGhd5whStmqWvYF4OjQROlmv9SMFVt37qoCBdqRffuuklWYQlCNnEs2ZaIBD1kZRNnikiZOS1eqgkar0iw==", + "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.5.1", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz", + "integrity": "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "dev": true, + "license": "MIT", + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/webpack": { + "version": "5.106.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.106.0.tgz", + "integrity": "sha512-Pkx5joZ9RrdgO5LBkyX1L2ZAJeK/Taz3vqZ9CbcP0wS5LEMx5QkKsEwLl29QJfihZ+DKRBFldzy1O30pJ1MDpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.8", + "@types/json-schema": "^7.0.15", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.16.0", + "acorn-import-phases": "^1.0.3", + "browserslist": "^4.28.1", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.20.0", + "es-module-lexer": "^2.0.0", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.3.1", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^4.3.3", + "tapable": "^2.3.0", + "terser-webpack-plugin": "^5.3.17", + "watchpack": "^2.5.1", + "webpack-sources": "^3.3.4" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-node-externals": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/webpack-node-externals/-/webpack-node-externals-3.0.0.tgz", + "integrity": "sha512-LnL6Z3GGDPht/AigwRh2dvL9PQPFQ8skEpVrWZXLWBYmqcaojHNN0onvHzie6rq7EWKrrBfPYqNEzTJgiwEQDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/webpack-sources": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.4.tgz", + "integrity": "sha512-7tP1PdV4vF+lYPnkMR0jMY5/la2ub5Fc/8VQrrU+lXkiM6C4TjVfGw7iKfyhnTQOsD+6Q/iKw0eFciziRgD58Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack/node_modules/ajv-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/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==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.20", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz", + "integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==", + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "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/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yoctocolors-cjs": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz", + "integrity": "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 00000000..3040849c --- /dev/null +++ b/package.json @@ -0,0 +1,39 @@ +{ + "name": "wallet-transfer-saga", + "private": true, + "workspaces": [ + "apps/*", + "libs/*" + ], + "scripts": { + "build": "yarn workspaces run build", + "test": "jest --config jest.config.js", + "test:cov": "jest --config jest.config.js --coverage", + "docker:build-base": "docker build -f Dockerfile.base -t wallet-transfer-saga-base .", + "docker:up": "npm run docker:build-base && docker compose up --build -d", + "docker:down": "docker compose down -v" + }, + "devDependencies": { + "@nestjs/cli": "^11.0.0", + "@nestjs/testing": "^11.1.18", + "@types/jest": "^29.5.14", + "@types/node": "^22.0.0", + "jest": "^29.7.0", + "ts-jest": "^29.4.9", + "ts-loader": "^9.5.7", + "typescript": "^5.4.5" + }, + "dependencies": { + "@nestjs/common": "^11.1.18", + "@nestjs/core": "^11.1.18", + "@nestjs/microservices": "^11.1.18", + "@nestjs/typeorm": "^11.0.1", + "class-transformer": "^0.5.1", + "class-validator": "^0.15.1", + "reflect-metadata": "^0.2.2", + "rxjs": "^7.8.2", + "tsconfig-paths": "^4.2.0", + "typeorm": "^0.3.28", + "uuid": "^13.0.0" + } +} diff --git a/scripts/seed.sql b/scripts/seed.sql new file mode 100644 index 00000000..bac9bde7 --- /dev/null +++ b/scripts/seed.sql @@ -0,0 +1,18 @@ +-- Wallets de prueba para validación e2e +-- wallet-a: balance 100.00 PEN (10000 centavos) +-- wallet-b: balance 50.00 PEN (5000 centavos) + +CREATE TABLE IF NOT EXISTS wallets ( + id UUID PRIMARY KEY, + balance BIGINT NOT NULL, + currency CHAR(3) NOT NULL, + version INTEGER NOT NULL DEFAULT 1, + "createdAt" TIMESTAMPTZ NOT NULL DEFAULT NOW(), + "updatedAt" TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +INSERT INTO wallets (id, balance, currency, version) +VALUES + ('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', 10000, 'PEN', 1), + ('bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', 5000, 'PEN', 1) +ON CONFLICT (id) DO NOTHING; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..c3932006 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "module": "commonjs", + "declaration": true, + "removeComments": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "allowSyntheticDefaultImports": true, + "target": "ES2022", + "sourceMap": true, + "outDir": "./dist", + "baseUrl": "./", + "incremental": true, + "skipLibCheck": true, + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "paths": { + "@contracts/*": ["libs/contracts/*"], + "@common/*": ["libs/common/*"], + "@helpers/*": ["libs/helpers/*"] + } + } +}